From: =?utf-8?q?Timo_R=C3=B6hling?= <roehling@debian.org>
Date: Thu, 28 Aug 2025 09:43:20 +0200
Subject: Replace pkg_resources usage with importlib
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

Author: Steven! Ragnarök
Last-Update: 2025-30-06
Origin: https://github.com/ros-infrastructure/bloom/pull/747
Forwarded: yes
Bug-Debian: https://bugs.debian.org/1083753
---
 bloom/__init__.py                    | 11 ++++++++---
 bloom/commands/generate.py           | 10 +++++++---
 bloom/commands/release.py            |  8 ++++++--
 bloom/generators/common.py           | 10 +++++++---
 bloom/generators/debian/generator.py | 16 +++++++++++-----
 bloom/generators/rpm/generator.py    | 17 +++++++++++------
 stdeb.cfg                            |  2 +-
 test/utils/common.py                 | 13 +++++++++++--
 8 files changed, 62 insertions(+), 25 deletions(-)

diff --git a/bloom/__init__.py b/bloom/__init__.py
index 6d917ab..605797f 100755
--- a/bloom/__init__.py
+++ b/bloom/__init__.py
@@ -1,8 +1,13 @@
+import sys
+
 try:
-    import pkg_resources
+    if sys.version_info[0:2] < (3, 10):
+        import importlib_metadata
+    else:
+        import importlib.metadata as importlib_metadata
     try:
-        __version__ = pkg_resources.require("bloom")[0].version
-    except pkg_resources.DistributionNotFound:
+        __version__ = importlib_metadata.metadata("bloom").get("version")
+    except importlib_metadata.PackageNotFoundError:
         __version__ = 'unset'
 except (ImportError, OSError):
     __version__ = 'unset'
diff --git a/bloom/commands/generate.py b/bloom/commands/generate.py
index 647fe8c..9e1c3df 100644
--- a/bloom/commands/generate.py
+++ b/bloom/commands/generate.py
@@ -35,23 +35,27 @@ from __future__ import print_function
 
 import argparse
 import sys
-import pkg_resources
 
 from bloom.util import add_global_arguments
 from bloom.util import handle_global_arguments
 
+if sys.version_info[0:2] < (3, 10):
+    from importlib_metadata import entry_points
+else:
+    from importlib.metadata import entry_points
+
 BLOOM_GENERATE_CMDS_GROUP = 'bloom.generate_cmds'
 
 
 def list_generator_commands():
     generators = []
-    for entry_point in pkg_resources.iter_entry_points(group=BLOOM_GENERATE_CMDS_GROUP):
+    for entry_point in entry_points(group=BLOOM_GENERATE_CMDS_GROUP):
         generators.append(entry_point.name)
     return generators
 
 
 def load_generator_description(generator_name):
-    for entry_point in pkg_resources.iter_entry_points(group=BLOOM_GENERATE_CMDS_GROUP):
+    for entry_point in entry_points(group=BLOOM_GENERATE_CMDS_GROUP):
         if entry_point.name == generator_name:
             return entry_point.load()
 
diff --git a/bloom/commands/release.py b/bloom/commands/release.py
index 35837e0..4c83d10 100644
--- a/bloom/commands/release.py
+++ b/bloom/commands/release.py
@@ -39,7 +39,6 @@ import atexit
 import datetime
 import difflib
 import os
-import pkg_resources
 import platform
 import shutil
 import subprocess
@@ -139,6 +138,11 @@ except ImportError:
 
 from catkin_pkg.changelog import get_changelog_from_path
 
+if sys.version_info[0:2] < (3, 10):
+    import importlib_metadata
+else:
+    import importlib.metadata as importlib_metadata
+
 _repositories = {}
 
 _success = get_success_prefix()
@@ -865,7 +869,7 @@ Versions of tools used:
         bloom_v=bloom.__version__,
         catkin_pkg_v=catkin_pkg.__version__,
         # Until https://github.com/ros-infrastructure/rosdistro/issues/16
-        rosdistro_v=pkg_resources.require("rosdistro")[0].version,
+        rosdistro_v=importlib_metadata.metadata("rosdistro").get("version"),
         rosdep_v=rosdep2.__version__,
         vcstools_v=vcstools.__version__.version
     )
diff --git a/bloom/generators/common.py b/bloom/generators/common.py
index 09ba51d..4d02618 100644
--- a/bloom/generators/common.py
+++ b/bloom/generators/common.py
@@ -32,7 +32,6 @@
 
 from __future__ import print_function
 
-import pkg_resources
 import sys
 import traceback
 
@@ -57,19 +56,24 @@ except ImportError as err:
     debug(traceback.format_exc())
     error("rosdep was not detected, please install it.", exit=True)
 
+if sys.version_info[0:2] < (3, 10):
+    from importlib_metadata import entry_points
+else:
+    from importlib.metadata import entry_points
+
 BLOOM_GROUP = 'bloom.generators'
 DEFAULT_ROS_DISTRO = 'indigo'
 
 
 def list_generators():
     generators = []
-    for entry_point in pkg_resources.iter_entry_points(group=BLOOM_GROUP):
+    for entry_point in entry_points(group=BLOOM_GROUP):
         generators.append(entry_point.name)
     return generators
 
 
 def load_generator(generator_name):
-    for entry_point in pkg_resources.iter_entry_points(group=BLOOM_GROUP):
+    for entry_point in entry_points(group=BLOOM_GROUP):
         if entry_point.name == generator_name:
             return entry_point.load()
 
diff --git a/bloom/generators/debian/generator.py b/bloom/generators/debian/generator.py
index c1abe49..8d998e9 100644
--- a/bloom/generators/debian/generator.py
+++ b/bloom/generators/debian/generator.py
@@ -37,7 +37,6 @@ import datetime
 import io
 import json
 import os
-import pkg_resources
 import re
 import shutil
 import sys
@@ -89,6 +88,12 @@ from bloom.util import execute_command
 from bloom.util import get_rfc_2822_date
 from bloom.util import maybe_continue
 
+if sys.version_info[0:2] < (3, 10):
+    import importlib_resources
+else:
+    import importlib.resources as importlib_resources
+
+
 try:
     from catkin_pkg.changelog import get_changelog_from_path
     from catkin_pkg.changelog import CHANGELOG_FILENAME
@@ -126,7 +131,8 @@ TEMPLATE_EXTENSION = '.em'
 
 
 def __place_template_folder(group, src, dst, gbp=False):
-    template_files = pkg_resources.resource_listdir(group, src)
+    template_files = [os.path.basename(file)
+                      for file in importlib_resources.files(f'{group}.{src.replace("/", ".")}').iterdir()]
     # For each template, place
     for template_file in template_files:
         if not gbp and os.path.basename(template_file) == 'gbp.conf.em':
@@ -134,14 +140,14 @@ def __place_template_folder(group, src, dst, gbp=False):
             continue
         template_path = os.path.join(src, template_file)
         template_dst = os.path.join(dst, template_file)
-        if pkg_resources.resource_isdir(group, template_path):
+        if importlib_resources.files(group).joinpath(template_path).is_dir():
             debug("Recursing on folder '{0}'".format(template_path))
             __place_template_folder(group, template_path, template_dst, gbp)
         else:
             try:
                 debug("Placing template '{0}'".format(template_path))
-                template = pkg_resources.resource_string(group, template_path)
-                template_abs_path = pkg_resources.resource_filename(group, template_path)
+                template = importlib_resources.files(group).joinpath(template_path).open().read()
+                template_abs_path = importlib_resources.files(group).joinpath(template_path)
             except IOError as err:
                 error("Failed to load template "
                       "'{0}': {1}".format(template_file, str(err)), exit=True)
diff --git a/bloom/generators/rpm/generator.py b/bloom/generators/rpm/generator.py
index 66c8f3f..00b51e4 100644
--- a/bloom/generators/rpm/generator.py
+++ b/bloom/generators/rpm/generator.py
@@ -37,12 +37,11 @@ import datetime
 import io
 import json
 import os
-import pkg_resources
 import re
 import shutil
 import sys
-import traceback
 import textwrap
+import traceback
 
 from dateutil import tz
 from packaging.version import Version
@@ -83,6 +82,11 @@ from bloom.util import code
 from bloom.util import execute_command
 from bloom.util import maybe_continue
 
+if sys.version_info[0:2] < (3, 10):
+    import importlib_resources
+else:
+    import importlib.resources as importlib_resources
+
 try:
     import rosdistro
 except ImportError as err:
@@ -102,19 +106,20 @@ TEMPLATE_EXTENSION = '.em'
 
 
 def __place_template_folder(group, src, dst, gbp=False):
-    template_files = pkg_resources.resource_listdir(group, src)
+    template_files = [os.path.basename(file)
+                      for file in importlib_resources.files(f'{group}.{src.replace("/", ".")}').iterdir()]
     # For each template, place
     for template_file in template_files:
         template_path = os.path.join(src, template_file)
         template_dst = os.path.join(dst, template_file)
-        if pkg_resources.resource_isdir(group, template_path):
+        if importlib_resources.files(group).joinpath(template_path).is_dir():
             debug("Recursing on folder '{0}'".format(template_path))
             __place_template_folder(group, template_path, template_dst, gbp)
         else:
             try:
                 debug("Placing template '{0}'".format(template_path))
-                template = pkg_resources.resource_string(group, template_path)
-                template_abs_path = pkg_resources.resource_filename(group, template_path)
+                template = importlib_resources.files(group).joinpath(template_path).open().read()
+                template_abs_path = importlib_resources.files(group).joinpath(template_path)
             except IOError as err:
                 error("Failed to load template "
                       "'{0}': {1}".format(template_file, str(err)), exit=True)
diff --git a/stdeb.cfg b/stdeb.cfg
index d986283..53b8d32 100755
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -1,7 +1,7 @@
 [DEFAULT]
 ; release with a high debinc to avoid conflict with upstream debian of the same release version
 Debian-Version: 100
-Depends3: python3-yaml, python3-empy, python3-packaging, python3-rosdep (>= 0.15.0), python3-rosdistro (>= 0.8.0), python3-vcstools (>= 0.1.22), python3-setuptools, python3-catkin-pkg (>= 0.4.3)
+Depends3: python3-yaml, python3-empy, python3-packaging, python3-rosdep (>= 0.15.0), python3-rosdistro (>= 0.8.0), python3-vcstools (>= 0.1.22), python3-setuptools, python3-catkin-pkg (>= 0.4.3), python3 (>= 3.10) | python3-importlib-metadata (>= 3.6), python3 (>= 3.10) | python3-importlib-resources (>= 5.0)
 Conflicts3: python-bloom
 Copyright-File: LICENSE.txt
 Suite3: focal jammy noble bookworm trixie
diff --git a/test/utils/common.py b/test/utils/common.py
index 670da77..dd9f6b2 100644
--- a/test/utils/common.py
+++ b/test/utils/common.py
@@ -196,7 +196,10 @@ def user_bloom(cmd, args=None, directory=None, auto_assert=True,
     assert type(args) in [list, tuple, str], \
         "user_bloom args takes [list, tuple, str] only, got " + \
         str(type(args))
-    from pkg_resources import load_entry_point
+    if sys.version_info[0:2] < (3, 10):
+        from importlib_metadata import entry_points
+    else:
+        from importlib.metadata import entry_points
     from bloom import __version__ as ver
     if not cmd.startswith('git-bloom-'):
         cmd = 'git-bloom-' + cmd
@@ -206,7 +209,13 @@ def user_bloom(cmd, args=None, directory=None, auto_assert=True,
         args = list(args)
     with change_directory(directory if directory is not None else os.getcwd()):
         with redirected_stdio() as (out, err):
-            func = load_entry_point('bloom==' + ver, 'console_scripts', cmd)
+            # importlib can't filter entry points by distribution because they
+            # don't compare. So get all matching entry points and filter by
+            # distribution version and name after.
+            eps = [ep for ep in entry_points(group="console_scripts", name=cmd)
+                   if ep.dist.version == ver and ep.dist.name == 'bloom']
+            assert len(eps) == 1, f"Multiple entry points found for command '{cmd}'."
+            func = eps[0].load()
             try:
                 with change_environ(env):
                     ret = func(args) or 0
