Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert from using importlib.metadata back to pkg_resources #587

Merged
merged 1 commit into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 38 additions & 40 deletions colcon_core/extension_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,11 @@
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 @@ -51,26 +44,27 @@
colcon_extension_points.setdefault(EXTENSION_POINT_GROUP_NAME, None)

entry_points = defaultdict(dict)
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:
working_set = WorkingSet()
for dist in sorted(working_set):
entry_map = dist.get_entry_map()
for group_name in entry_map.keys():
# skip groups which are not registered as extension points
if entry_point.group not in colcon_extension_points:
if group_name not in colcon_extension_points:
continue

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)
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(

Check warning on line 59 in colcon_core/extension_point.py

View check run for this annotation

Codecov / codecov/patch

colcon_core/extension_point.py#L58-L59

Added lines #L58 - L59 were not covered by tests
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)}"

Check warning on line 65 in colcon_core/extension_point.py

View check run for this annotation

Codecov / codecov/patch

colcon_core/extension_point.py#L65

Added line #L65 was not covered by tests
entry_points[group_name][entry_point_name] = (
value, dist.project_name, getattr(dist, 'version', None))
return entry_points


Expand All @@ -82,21 +76,19 @@
:returns: mapping of extension point names to extension point values
:rtype: dict
"""
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]
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]

Check warning on line 82 in colcon_core/extension_point.py

View check run for this annotation

Codecov / codecov/patch

colcon_core/extension_point.py#L82

Added line #L82 was not covered by tests
logger.error(
f"Entry point '{group}.{entry_point.name}' is declared "
f"multiple times, '{entry_point.value}' overwriting "
f"multiple times, '{entry_point}' overwriting "
f"'{previous_entry_point}'")
extension_points[entry_point.name] = entry_point.value
return extension_points
value = entry_point.module_name
if entry_point.attrs:
value += f":{'.'.join(entry_point.attrs)}"
entry_points[entry_point.name] = value
return entry_points


def load_extension_points(group, *, excludes=None):
Expand Down Expand Up @@ -154,4 +146,10 @@
raise RuntimeError(
'The entry point name is listed in the environment variable '
f"'{EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name}'")
return EntryPoint(name, value, group).load()
if ':' in value:
module_name, attr = value.split(':', 1)
attrs = attr.split('.')
else:
module_name = value
attrs = ()
return EntryPoint(name, module_name, attrs).resolve()
14 changes: 4 additions & 10 deletions colcon_core/package_identification/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,11 @@
except ImportError:
from setuptools.config import read_configuration
except ImportError as e:
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'
from pkg_resources import get_distribution
from pkg_resources import parse_version
setuptools_version = get_distribution('setuptools').version

Check warning on line 95 in colcon_core/package_identification/python.py

View check run for this annotation

Codecov / codecov/patch

colcon_core/package_identification/python.py#L93-L95

Added lines #L93 - L95 were not covered by tests
minimum_version = '30.3.0'
if Version(setuptools_version) < Version(minimum_version):
if parse_version(setuptools_version) < parse_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 packaging.version import Version
from pkg_resources import parse_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 = Version(version)
extension_version = Version(caret_range[1:])
extension_point_version = parse_version(version)
extension_version = parse_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 Version('%d.%d.0' % (major, minor))
return parse_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 packaging.version import Version
from pkg_resources import parse_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 Version(pytest_version) >= Version('3.3.0'):
if parse_version(pytest_version) >= parse_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 Version(pytest_cov_version) >= Version('2.5.0'):
if parse_version(pytest_cov_version) >= parse_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 <web@dirk-thomas.net>

--- setup.cfg 2018-05-27 11:22:33.000000000 -0700
+++ setup.cfg.patched 2018-05-27 11:22:33.000000000 -0700
@@ -33,9 +33,12 @@
importlib-metadata; python_version < "3.8"
packaging
@@ -31,9 +31,12 @@
distlib
EmPy
pytest
- pytest-cov
- pytest-repeat
Expand Down
4 changes: 1 addition & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ 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 @@ -69,7 +67,7 @@ filterwarnings =
error
# Suppress deprecation warnings in other packages
ignore:lib2to3 package is deprecated::scspell
ignore:pkg_resources is deprecated as an API::colcon_core.entry_point
ignore:pkg_resources is deprecated as an API
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, python3 (>= 3.8) | python3-importlib-metadata
Depends3: python3-distlib, python3-empy, python3-packaging, python3-pytest, python3-setuptools
Recommends3: python3-pytest-cov
Suggests3: python3-pytest-repeat, python3-pytest-rerunfailures
Suite: bionic focal jammy stretch buster bullseye
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,6 +2,7 @@ addopts
apache
argparse
asyncio
attrs
autouse
basepath
bazqux
Expand Down Expand Up @@ -46,7 +47,6 @@ hardcodes
hookimpl
hookwrapper
https
importlib
isatty
iterdir
junit
Expand Down
80 changes: 38 additions & 42 deletions test/test_extension_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,100 +17,96 @@
from .environment_context import EnvironmentContext


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)
Group1 = EntryPoint('group1', 'g1')
Group2 = EntryPoint('group2', 'g2')


class Dist():

version = '0.0.0'
project_name = 'dist'

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

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

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


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


def distributions():
def working_set():
return [
Dist(iter_entry_points(group='group1')),
Dist([EntryPoint('extC', 'eC', Group2.name)]),
Dist([EntryPoint('extD', 'eD', 'groupX')]),
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')}}),
]


def test_all_extension_points():
with patch(
'colcon_core.extension_point.entry_points',
'colcon_core.extension_point.iter_entry_points',
side_effect=iter_entry_points
):
with patch(
'colcon_core.extension_point.distributions',
side_effect=distributions
'colcon_core.extension_point.WorkingSet',
side_effect=working_set
):
# 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'][0] == 'eA'
assert extension_points['group1']['extA'] == (
'eA', Dist.project_name, None)


def test_extension_point_blocklist():
# successful loading of extension point without a blocklist
with patch(
'colcon_core.extension_point.entry_points',
'colcon_core.extension_point.iter_entry_points',
side_effect=iter_entry_points
):
with patch(
'colcon_core.extension_point.distributions',
side_effect=distributions
'colcon_core.extension_point.WorkingSet',
side_effect=working_set
):
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, 'load', return_value=None) as load:
with patch.object(EntryPoint, 'resolve', return_value=None) as resolve:
load_extension_point('extA', 'eA', 'group1')
assert load.call_count == 1
assert resolve.call_count == 1

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

# entry point in a blocked group can't be loaded
load.reset_mock()
resolve.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 load.call_count == 0
assert resolve.call_count == 0

# entry point listed in the blocklist can't be loaded
with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([
Expand All @@ -120,10 +116,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 load.call_count == 0
assert resolve.call_count == 0


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


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