diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py index 54191e7a..ff348ba5 100644 --- a/colcon_core/extension_point.py +++ b/colcon_core/extension_point.py @@ -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( @@ -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 @@ -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): @@ -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() diff --git a/colcon_core/package_identification/python.py b/colcon_core/package_identification/python.py index 9d5e467b..0b11c8aa 100644 --- a/colcon_core/package_identification/python.py +++ b/colcon_core/package_identification/python.py @@ -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 ' \ diff --git a/colcon_core/plugin_system.py b/colcon_core/plugin_system.py index 4794e2a9..7d3934a3 100644 --- a/colcon_core/plugin_system.py +++ b/colcon_core/plugin_system.py @@ -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__) @@ -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) @@ -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)) diff --git a/colcon_core/task/python/test/pytest.py b/colcon_core/task/python/test/pytest.py index 6b51d267..335e2140 100644 --- a/colcon_core/task/python/test/pytest.py +++ b/colcon_core/task/python/test/pytest.py @@ -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): @@ -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'), @@ -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', ] diff --git a/debian/patches/setup.cfg.patch b/debian/patches/setup.cfg.patch index 14d9486e..add781c2 100644 --- a/debian/patches/setup.cfg.patch +++ b/debian/patches/setup.cfg.patch @@ -5,9 +5,9 @@ Author: Dirk Thomas --- 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 diff --git a/setup.cfg b/setup.cfg index 7da68786..0281aeeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 @@ -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 diff --git a/stdeb.cfg b/stdeb.cfg index 54f6e44a..9c4793cd 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -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 diff --git a/test/spell_check.words b/test/spell_check.words index c3414f9e..a1cb3d99 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -2,7 +2,6 @@ addopts apache argparse asyncio -attrs autouse basepath bazqux @@ -47,6 +46,7 @@ hardcodes hookimpl hookwrapper https +importlib isatty iterdir junit diff --git a/test/test_extension_point.py b/test/test_extension_point.py index 73b6a2d4..7111b796 100644 --- a/test/test_extension_point.py +++ b/test/test_extension_point.py @@ -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([ @@ -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': @@ -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'}