Skip to content

Commit 29f809f

Browse files
authored
Remove runtime pkg_resources dependency in default and server_ingester (#7057)
## Motivation for features / changes Fixes #7003. `pkg_resources` is removed in newer setuptools, which can break TensorBoard at import/runtime. This change removes runtime reliance on `pkg_resources` in the two affected code paths. ## Technical description of changes - Replaced dynamic plugin discovery in `tensorboard/default.py`: - from `pkg_resources.iter_entry_points(...)` - to `importlib.metadata.entry_points(...)` with compatibility handling for different Python return shapes. - Replaced version parsing in `tensorboard/data/server_ingester.py`: - from `pkg_resources.parse_version(...)` - to `packaging.version.parse(...)`. - Updated related tests: - `tensorboard/default_test.py` now patches `_iter_entry_points` and uses `load()`-style fake entry points. - `tensorboard/version_test.py` now validates PEP 440 behavior using `packaging.version`. - Updated Bazel deps to use `expect_packaging_installed` where `packaging` is now required. ## Detailed steps to verify changes work correctly (as executed by you) Executed locally in an isolated venv: - `python -m py_compile tensorboard/default.py tensorboard/default_test.py tensorboard/data/server_ingester.py tensorboard/version_test.py` - `PYTHONPATH=. python tensorboard/version_test.py` (passes) Attempted but environment-limited locally: - `bazel test //tensorboard:version_test //tensorboard:default_test //tensorboard/data:server_ingester_test` (bazel not available in local shell) - Direct execution of `default_test.py` and `server_ingester_test.py` without Bazel-generated artifacts hit local environment/import constraints. ## Alternate designs / implementations considered (or N/A) - N/A
1 parent 3ad0c35 commit 29f809f

6 files changed

Lines changed: 39 additions & 71 deletions

File tree

tensorboard/BUILD

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ py_library(
309309
srcs = ["default.py"],
310310
srcs_version = "PY3",
311311
deps = [
312-
"//tensorboard:expect_pkg_resources_installed",
313312
"//tensorboard/backend:experimental_plugin",
314313
"//tensorboard/plugins/audio:audio_plugin",
315314
"//tensorboard/plugins/core:core_plugin",
@@ -347,7 +346,6 @@ py_test(
347346
deps = [
348347
":default",
349348
":test",
350-
"//tensorboard:expect_pkg_resources_installed",
351349
"//tensorboard/plugins:base_plugin",
352350
],
353351
)
@@ -367,7 +365,7 @@ py_test(
367365
deps = [
368366
":test",
369367
":version",
370-
"//tensorboard:expect_pkg_resources_installed",
368+
"//tensorboard:expect_packaging_installed",
371369
],
372370
)
373371

@@ -461,10 +459,10 @@ py_library(name = "expect_absl_logging_installed")
461459
# `pip install absl-py`
462460
py_library(name = "expect_absl_testing_absltest_installed")
463461

464-
# This is a dummy rule used as a pkg-resources dependency in open-source.
465-
# We expect pkg-resources to already be installed on the system, e.g., via
466-
# `pip install setuptools`.
467-
py_library(name = "expect_pkg_resources_installed")
462+
# This is a dummy rule used as a packaging dependency in open-source.
463+
# We expect packaging to already be installed on the system, e.g. via
464+
# `pip install packaging`.
465+
py_library(name = "expect_packaging_installed")
468466

469467
# This is a dummy rule used as a pandas dependency in open-source.
470468
# We expect pandas to already be installed on the system, e.g. via

tensorboard/data/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ py_library(
6262
":grpc_provider",
6363
":ingester",
6464
"//tensorboard:expect_grpc_installed",
65-
"//tensorboard:expect_pkg_resources_installed",
65+
"//tensorboard:expect_packaging_installed",
6666
"//tensorboard/data/proto:protos_all_py_pb2",
6767
"//tensorboard/util:tb_logging",
6868
],

tensorboard/data/server_ingester.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@
2222
import time
2323

2424
import grpc
25-
import pkg_resources
25+
from packaging import version as packaging_version
2626

2727
from tensorboard.data import grpc_provider
2828
from tensorboard.data import ingester
2929
from tensorboard.data.proto import data_provider_pb2
3030
from tensorboard.util import tb_logging
3131

32-
3332
logger = tb_logging.get_logger()
3433

3534
# If this environment variable is non-empty, it will be used as the path to the
@@ -202,7 +201,7 @@ def _maybe_read_file(path):
202201

203202

204203
def _make_stub(addr, channel_creds_type):
205-
(creds, options) = channel_creds_type.channel_config()
204+
creds, options = channel_creds_type.channel_config()
206205
options.append(("grpc.max_receive_message_length", 1024 * 1024 * 256))
207206
channel = grpc.secure_channel(addr, creds, options=options)
208207
return grpc_provider.make_stub(channel)
@@ -231,9 +230,7 @@ def __init__(self, path, version):
231230
"""
232231
self._path = path
233232
self._version = (
234-
pkg_resources.parse_version(version)
235-
if version is not None
236-
else version
233+
packaging_version.parse(version) if version is not None else version
237234
)
238235

239236
@property
@@ -260,7 +257,7 @@ def at_least_version(self, required_version):
260257
"""
261258
if self._version is None:
262259
return True
263-
return self._version >= pkg_resources.parse_version(required_version)
260+
return self._version >= packaging_version.parse(required_version)
264261

265262

266263
def get_server_binary():

tensorboard/default.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@
2424
for less repetition.
2525
"""
2626

27-
2827
import logging
29-
30-
import pkg_resources
28+
from importlib import metadata
3129

3230
from tensorboard.plugins.audio import audio_plugin
3331
from tensorboard.plugins.core import core_plugin
@@ -46,7 +44,6 @@
4644
from tensorboard.plugins.mesh import mesh_plugin
4745
from tensorboard.plugins.wit_redirect import wit_redirect_plugin
4846

49-
5047
logger = logging.getLogger(__name__)
5148

5249

@@ -119,8 +116,17 @@ def get_dynamic_plugins():
119116
[1]: https://packaging.python.org/specifications/entry-points/
120117
"""
121118
return [
122-
entry_point.resolve()
123-
for entry_point in pkg_resources.iter_entry_points(
124-
"tensorboard_plugins"
125-
)
119+
entry_point.load()
120+
for entry_point in _iter_entry_points("tensorboard_plugins")
126121
]
122+
123+
124+
def _iter_entry_points(group):
125+
"""Returns entry points for a given group across Python versions."""
126+
entry_points = metadata.entry_points()
127+
# In newer Python versions, `metadata.entry_points()` returns an
128+
# `EntryPoints` object with a `select()` method.
129+
# Before "selectable" entry points existed, it would return a dictionary.
130+
if hasattr(entry_points, "select"):
131+
return entry_points.select(group=group)
132+
return entry_points.get(group, ())

tensorboard/default_test.py

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,30 @@
1414
# ==============================================================================
1515
"""Unit tests for `tensorboard.default`."""
1616

17-
1817
from unittest import mock
1918

20-
import pkg_resources
21-
2219
from tensorboard import default
23-
from tensorboard.plugins import base_plugin
2420
from tensorboard import test
2521

2622

27-
class FakePlugin(base_plugin.TBPlugin):
28-
"""FakePlugin for testing."""
29-
30-
plugin_name = "fake"
31-
32-
33-
class FakeEntryPoint(pkg_resources.EntryPoint):
34-
"""EntryPoint class that fake loads FakePlugin."""
35-
36-
@classmethod
37-
def create(cls):
38-
"""Creates an instance of FakeEntryPoint.
39-
40-
Returns:
41-
instance of FakeEntryPoint
42-
"""
43-
return cls("foo", "bar")
44-
45-
def resolve(self):
46-
"""Returns FakePlugin instead of resolving module.
23+
class FakeEntryPoint:
24+
def __init__(self, value):
25+
self._value = value
4726

48-
Returns:
49-
FakePlugin
50-
"""
51-
return FakePlugin
27+
def load(self):
28+
return self._value
5229

5330

5431
class DefaultTest(test.TestCase):
55-
@mock.patch.object(pkg_resources, "iter_entry_points")
32+
@mock.patch.object(default, "_iter_entry_points")
5633
def test_get_dynamic_plugin(self, mock_iter_entry_points):
57-
mock_iter_entry_points.return_value = [FakeEntryPoint.create()]
34+
fake_plugin = object()
35+
mock_iter_entry_points.return_value = [FakeEntryPoint(fake_plugin)]
5836

5937
actual_plugins = default.get_dynamic_plugins()
6038

6139
mock_iter_entry_points.assert_called_with("tensorboard_plugins")
62-
self.assertEqual(actual_plugins, [FakePlugin])
40+
self.assertEqual(actual_plugins, [fake_plugin])
6341

6442

6543
if __name__ == "__main__":

tensorboard/version_test.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515

16-
import pkg_resources
16+
from packaging import version as packaging_version
1717

1818
from tensorboard import test as tb_test
1919
from tensorboard import version
@@ -22,22 +22,11 @@
2222
class VersionTest(tb_test.TestCase):
2323
def test_valid_pep440_version(self):
2424
"""Ensure that our version is PEP 440-compliant."""
25-
# pkg_resources.parse_version() doesn't have a public return type,
26-
# so we get a handle to it by parsing known good and bad versions.
27-
#
28-
# Note: depending on the version of the module (which is bundled
29-
# with setuptools), when called with a non-compliant version, it
30-
# either returns a `LegacyVersion` (setuptools < 66) or raises an
31-
# `InvalidVersion` exception (setuptools >= 66). Handle both cases.
32-
compliant_version = pkg_resources.parse_version("1.0.0")
33-
try:
34-
legacy_version = pkg_resources.parse_version("arbitrary string")
35-
except Exception:
36-
legacy_version = None
37-
self.assertNotEqual(type(compliant_version), type(legacy_version))
38-
39-
tensorboard_version = pkg_resources.parse_version(version.VERSION)
40-
self.assertIsInstance(tensorboard_version, type(compliant_version))
25+
with self.assertRaises(packaging_version.InvalidVersion):
26+
packaging_version.parse("arbitrary string")
27+
28+
tensorboard_version = packaging_version.parse(version.VERSION)
29+
self.assertIsInstance(tensorboard_version, packaging_version.Version)
4130

4231

4332
if __name__ == "__main__":

0 commit comments

Comments
 (0)