diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py index ff348ba5..54191e7a 100644 --- a/colcon_core/extension_point.py +++ b/colcon_core/extension_point.py @@ -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( @@ -51,26 +44,27 @@ def get_all_extension_points(): 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( + 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)) return entry_points @@ -82,21 +76,19 @@ def get_extension_points(group): :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] 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): @@ -154,4 +146,10 @@ 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}'") - 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() diff --git a/colcon_core/package_identification/python.py b/colcon_core/package_identification/python.py index 0b11c8aa..9d5e467b 100644 --- a/colcon_core/package_identification/python.py +++ b/colcon_core/package_identification/python.py @@ -90,17 +90,11 @@ def get_configuration(setup_cfg): 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 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 ' \ diff --git a/colcon_core/plugin_system.py b/colcon_core/plugin_system.py index 7d3934a3..4794e2a9 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 packaging.version import Version +from pkg_resources import parse_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 = 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) @@ -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)) diff --git a/colcon_core/task/python/test/pytest.py b/colcon_core/task/python/test/pytest.py index 335e2140..6b51d267 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 packaging.version import Version +from pkg_resources import parse_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 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'), @@ -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', ] diff --git a/debian/patches/setup.cfg.patch b/debian/patches/setup.cfg.patch index add781c2..14d9486e 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 -@@ -33,9 +33,12 @@ - importlib-metadata; python_version < "3.8" - packaging +@@ -31,9 +31,12 @@ + distlib + EmPy pytest - pytest-cov - pytest-repeat diff --git a/setup.cfg b/setup.cfg index 0281aeeb..7da68786 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 @@ -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 diff --git a/stdeb.cfg b/stdeb.cfg index d6fda2f2..e274f195 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, 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 diff --git a/test/spell_check.words b/test/spell_check.words index a1cb3d99..c3414f9e 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -2,6 +2,7 @@ addopts apache argparse asyncio +attrs autouse basepath bazqux @@ -46,7 +47,6 @@ hardcodes hookimpl hookwrapper https -importlib isatty iterdir junit diff --git a/test/test_extension_point.py b/test/test_extension_point.py index 7111b796..73b6a2d4 100644 --- a/test/test_extension_point.py +++ b/test/test_extension_point.py @@ -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([ @@ -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': @@ -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'}