Skip to content

Commit

Permalink
Restore use of importlib.metadata instead of pkg_resources (#589)
Browse files Browse the repository at this point in the history
This reverts commit 731092b.
This restores commit a9de909.
This restores commit 5f9f5ff.
This restores commit 7b70e61.
  • Loading branch information
cottsay authored Oct 3, 2023
1 parent f2c3296 commit 20a5e9f
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 93 deletions.
78 changes: 40 additions & 38 deletions colcon_core/extension_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
import os
import traceback

try:
from importlib.metadata import distributions
from importlib.metadata import EntryPoint
from importlib.metadata import entry_points
except ImportError:
# TODO: Drop this with Python 3.7 support
from importlib_metadata import distributions
from importlib_metadata import EntryPoint
from importlib_metadata import entry_points

from colcon_core.environment_variable import EnvironmentVariable
from colcon_core.logging import colcon_logger
from pkg_resources import EntryPoint
from pkg_resources import iter_entry_points
from pkg_resources import WorkingSet

"""Environment variable to block extensions"""
EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE = EnvironmentVariable(
Expand Down Expand Up @@ -44,27 +51,26 @@ def get_all_extension_points():
colcon_extension_points.setdefault(EXTENSION_POINT_GROUP_NAME, None)

entry_points = defaultdict(dict)
working_set = WorkingSet()
for dist in sorted(working_set):
entry_map = dist.get_entry_map()
for group_name in entry_map.keys():
seen = set()
for dist in distributions():
dist_name = dist.metadata['Name']
if dist_name in seen:
continue
seen.add(dist_name)
for entry_point in dist.entry_points:
# skip groups which are not registered as extension points
if group_name not in colcon_extension_points:
if entry_point.group not in colcon_extension_points:
continue

group = entry_map[group_name]
for entry_point_name, entry_point in group.items():
if entry_point_name in entry_points[group_name]:
previous = entry_points[group_name][entry_point_name]
logger.error(
f"Entry point '{group_name}.{entry_point_name}' is "
f"declared multiple times, '{entry_point}' "
f"overwriting '{previous}'")
value = entry_point.module_name
if entry_point.attrs:
value += f":{'.'.join(entry_point.attrs)}"
entry_points[group_name][entry_point_name] = (
value, dist.project_name, getattr(dist, 'version', None))
if entry_point.name in entry_points[entry_point.group]:
previous = entry_points[entry_point.group][entry_point.name]
logger.error(
f"Entry point '{entry_point.group}.{entry_point.name}' is "
f"declared multiple times, '{entry_point.value}' "
f"from '{dist._path}' "
f"overwriting '{previous}'")
entry_points[entry_point.group][entry_point.name] = \
(entry_point.value, dist_name, dist.version)
return entry_points


Expand All @@ -76,19 +82,21 @@ def get_extension_points(group):
:returns: mapping of extension point names to extension point values
:rtype: dict
"""
entry_points = {}
for entry_point in iter_entry_points(group=group):
if entry_point.name in entry_points:
previous_entry_point = entry_points[entry_point.name]
extension_points = {}
try:
# Python 3.10 and newer
query = entry_points(group=group)
except TypeError:
query = entry_points().get(group, ())
for entry_point in query:
if entry_point.name in extension_points:
previous_entry_point = extension_points[entry_point.name]
logger.error(
f"Entry point '{group}.{entry_point.name}' is declared "
f"multiple times, '{entry_point}' overwriting "
f"multiple times, '{entry_point.value}' overwriting "
f"'{previous_entry_point}'")
value = entry_point.module_name
if entry_point.attrs:
value += f":{'.'.join(entry_point.attrs)}"
entry_points[entry_point.name] = value
return entry_points
extension_points[entry_point.name] = entry_point.value
return extension_points


def load_extension_points(group, *, excludes=None):
Expand Down Expand Up @@ -146,10 +154,4 @@ def load_extension_point(name, value, group):
raise RuntimeError(
'The entry point name is listed in the environment variable '
f"'{EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name}'")
if ':' in value:
module_name, attr = value.split(':', 1)
attrs = attr.split('.')
else:
module_name = value
attrs = ()
return EntryPoint(name, module_name, attrs).resolve()
return EntryPoint(name, value, group).load()
14 changes: 10 additions & 4 deletions colcon_core/package_identification/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,17 @@ def get_configuration(setup_cfg):
except ImportError:
from setuptools.config import read_configuration
except ImportError as e:
from pkg_resources import get_distribution
from pkg_resources import parse_version
setuptools_version = get_distribution('setuptools').version
try:
from importlib.metadata import distribution
except ImportError:
from importlib_metadata import distribution
from packaging.version import Version
try:
setuptools_version = distribution('setuptools').version
except ModuleNotFoundError:
setuptools_version = '0'
minimum_version = '30.3.0'
if parse_version(setuptools_version) < parse_version(minimum_version):
if Version(setuptools_version) < Version(minimum_version):
e.msg += ', ' \
"'setuptools' needs to be at least version " \
f'{minimum_version}, if a newer version is not available ' \
Expand Down
8 changes: 4 additions & 4 deletions colcon_core/plugin_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from colcon_core.extension_point import load_extension_points
from colcon_core.logging import colcon_logger
from pkg_resources import parse_version
from packaging.version import Version

logger = colcon_logger.getChild(__name__)

Expand Down Expand Up @@ -166,8 +166,8 @@ def satisfies_version(version, caret_range):
:raises RuntimeError: if the version doesn't match the caret range
"""
assert caret_range.startswith('^'), 'Only supports caret ranges'
extension_point_version = parse_version(version)
extension_version = parse_version(caret_range[1:])
extension_point_version = Version(version)
extension_version = Version(caret_range[1:])
next_extension_version = _get_upper_bound_caret_version(
extension_version)

Expand All @@ -192,4 +192,4 @@ def _get_upper_bound_caret_version(version):
minor = 0
else:
minor += 1
return parse_version('%d.%d.0' % (major, minor))
return Version('%d.%d.0' % (major, minor))
6 changes: 3 additions & 3 deletions colcon_core/task/python/test/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from colcon_core.task.python.test import has_test_dependency
from colcon_core.task.python.test import PythonTestingStepExtensionPoint
from colcon_core.verb.test import logger
from pkg_resources import parse_version
from packaging.version import Version


class PytestPythonTestingStep(PythonTestingStepExtensionPoint):
Expand Down Expand Up @@ -64,7 +64,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102
# use -o option only when available
# https://github.com/pytest-dev/pytest/blob/3.3.0/CHANGELOG.rst
from pytest import __version__ as pytest_version
if parse_version(pytest_version) >= parse_version('3.3.0'):
if Version(pytest_version) >= Version('3.3.0'):
args += [
'-o', 'cache_dir=' + str(PurePosixPath(
*(Path(context.args.build_base).parts)) / '.pytest_cache'),
Expand Down Expand Up @@ -95,7 +95,7 @@ async def step(self, context, env, setup_py_data): # noqa: D102
]
# use --cov-branch option only when available
# https://github.com/pytest-dev/pytest-cov/blob/v2.5.0/CHANGELOG.rst
if parse_version(pytest_cov_version) >= parse_version('2.5.0'):
if Version(pytest_cov_version) >= Version('2.5.0'):
args += [
'--cov-branch',
]
Expand Down
6 changes: 3 additions & 3 deletions debian/patches/setup.cfg.patch
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Author: Dirk Thomas <[email protected]>

--- setup.cfg 2018-05-27 11:22:33.000000000 -0700
+++ setup.cfg.patched 2018-05-27 11:22:33.000000000 -0700
@@ -31,9 +31,12 @@
distlib
EmPy
@@ -33,9 +33,12 @@
importlib-metadata; python_version < "3.8"
packaging
pytest
- pytest-cov
- pytest-repeat
Expand Down
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ install_requires =
coloredlogs; sys_platform == 'win32'
distlib
EmPy
importlib-metadata; python_version < "3.8"
packaging
# the pytest dependency and its extensions are provided for convenience
# even though they are only conditional
pytest
Expand Down Expand Up @@ -67,7 +69,7 @@ filterwarnings =
error
# Suppress deprecation warnings in other packages
ignore:lib2to3 package is deprecated::scspell
ignore:pkg_resources is deprecated as an API
ignore:pkg_resources is deprecated as an API::colcon_core.entry_point
ignore:SelectableGroups dict interface is deprecated::flake8
ignore:The loop argument is deprecated::asyncio
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pydocstyle
Expand Down
2 changes: 1 addition & 1 deletion stdeb.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[colcon-core]
No-Python2:
Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools
Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools, python3 (>= 3.8) | python3-importlib-metadata
Recommends3: python3-pytest-cov
Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures
Suite: focal jammy bullseye bookworm
Expand Down
2 changes: 1 addition & 1 deletion test/spell_check.words
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ addopts
apache
argparse
asyncio
attrs
autouse
basepath
bazqux
Expand Down Expand Up @@ -47,6 +46,7 @@ hardcodes
hookimpl
hookwrapper
https
importlib
isatty
iterdir
junit
Expand Down
80 changes: 42 additions & 38 deletions test/test_extension_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,96 +17,100 @@
from .environment_context import EnvironmentContext


Group1 = EntryPoint('group1', 'g1')
Group2 = EntryPoint('group2', 'g2')
Group1 = EntryPoint('group1', 'g1', EXTENSION_POINT_GROUP_NAME)
Group2 = EntryPoint('group2', 'g2', EXTENSION_POINT_GROUP_NAME)
ExtA = EntryPoint('extA', 'eA', Group1.name)
ExtB = EntryPoint('extB', 'eB', Group1.name)


class Dist():

project_name = 'dist'
version = '0.0.0'

def __init__(self, group_name, group):
self._group_name = group_name
self._group = group
def __init__(self, entry_points):
self.metadata = {'Name': f'dist-{id(self)}'}
self._entry_points = entry_points

def __lt__(self, other):
return self._group_name < other._group_name
@property
def entry_points(self):
return list(self._entry_points)

def get_entry_map(self):
return self._group
@property
def name(self):
return self.metadata['Name']


def iter_entry_points(*, group):
def iter_entry_points(*, group=None):
if group == EXTENSION_POINT_GROUP_NAME:
return [Group1, Group2]
assert group == Group1.name
ep1 = EntryPoint('extA', 'eA')
ep2 = EntryPoint('extB', 'eB')
return [ep1, ep2]
elif group == Group1.name:
return [ExtA, ExtB]
assert not group
return {
EXTENSION_POINT_GROUP_NAME: [Group1, Group2],
Group1.name: [ExtA, ExtB],
}


def working_set():
def distributions():
return [
Dist('group1', {
'group1': {ep.name: ep for ep in iter_entry_points(group='group1')}
}),
Dist('group2', {'group2': {'extC': EntryPoint('extC', 'eC')}}),
Dist('groupX', {'groupX': {'extD': EntryPoint('extD', 'eD')}}),
Dist(iter_entry_points(group='group1')),
Dist([EntryPoint('extC', 'eC', Group2.name)]),
Dist([EntryPoint('extD', 'eD', 'groupX')]),
]


def test_all_extension_points():
with patch(
'colcon_core.extension_point.iter_entry_points',
'colcon_core.extension_point.entry_points',
side_effect=iter_entry_points
):
with patch(
'colcon_core.extension_point.WorkingSet',
side_effect=working_set
'colcon_core.extension_point.distributions',
side_effect=distributions
):
# successfully load a known entry point
extension_points = get_all_extension_points()
assert set(extension_points.keys()) == {'group1', 'group2'}
assert set(extension_points['group1'].keys()) == {'extA', 'extB'}
assert extension_points['group1']['extA'] == (
'eA', Dist.project_name, None)
assert extension_points['group1']['extA'][0] == 'eA'


def test_extension_point_blocklist():
# successful loading of extension point without a blocklist
with patch(
'colcon_core.extension_point.iter_entry_points',
'colcon_core.extension_point.entry_points',
side_effect=iter_entry_points
):
with patch(
'colcon_core.extension_point.WorkingSet',
side_effect=working_set
'colcon_core.extension_point.distributions',
side_effect=distributions
):
extension_points = get_extension_points('group1')
assert 'extA' in extension_points.keys()
extension_point = extension_points['extA']
assert extension_point == 'eA'

with patch.object(EntryPoint, 'resolve', return_value=None) as resolve:
with patch.object(EntryPoint, 'load', return_value=None) as load:
load_extension_point('extA', 'eA', 'group1')
assert resolve.call_count == 1
assert load.call_count == 1

# successful loading of entry point not in blocklist
resolve.reset_mock()
load.reset_mock()
with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([
'group1.extB', 'group2.extC'])
):
load_extension_point('extA', 'eA', 'group1')
assert resolve.call_count == 1
assert load.call_count == 1

# entry point in a blocked group can't be loaded
resolve.reset_mock()
load.reset_mock()
with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST='group1'):
with pytest.raises(RuntimeError) as e:
load_extension_point('extA', 'eA', 'group1')
assert 'The entry point group name is listed in the environment ' \
'variable' in str(e.value)
assert resolve.call_count == 0
assert load.call_count == 0

# entry point listed in the blocklist can't be loaded
with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([
Expand All @@ -116,10 +120,10 @@ def test_extension_point_blocklist():
load_extension_point('extA', 'eA', 'group1')
assert 'The entry point name is listed in the environment ' \
'variable' in str(e.value)
assert resolve.call_count == 0
assert load.call_count == 0


def entry_point_resolve(self, *args, **kwargs):
def entry_point_load(self, *args, **kwargs):
if self.name == 'exception':
raise Exception('entry point raising exception')
if self.name == 'runtime_error':
Expand All @@ -129,7 +133,7 @@ def entry_point_resolve(self, *args, **kwargs):
return DEFAULT


@patch.object(EntryPoint, 'resolve', entry_point_resolve)
@patch.object(EntryPoint, 'load', entry_point_load)
@patch(
'colcon_core.extension_point.get_extension_points',
return_value={'exception': 'a', 'runtime_error': 'b', 'success': 'c'}
Expand Down

0 comments on commit 20a5e9f

Please sign in to comment.