From 1d2352651a9f71610743362552e259b6c04b476d Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 2 Oct 2023 11:17:21 -0500 Subject: [PATCH] Revert from using importlib.metadata back to pkg_resources It appears that we accidentally shipped this incompatible change on Ubuntu Bionic, which is currently under ESM. While we do intend to drop mainline support for Bionic, we should leave it in a good state. This reverts commit a9de9091ae1d08fda52eb20d0027259fa3dd2ebe. This reverts commit 5f9f5fff95b8c6021933d7d5419879a9230b9462. This reverts commit 7b70e61175ab735ea5448ab6e88b4882975566b5. --- colcon_core/extension_point.py | 78 ++++++++++--------- colcon_core/package_identification/python.py | 14 +--- colcon_core/plugin_system.py | 8 +- colcon_core/task/python/test/pytest.py | 6 +- debian/patches/setup.cfg.patch | 6 +- setup.cfg | 4 +- stdeb.cfg | 2 +- test/spell_check.words | 2 +- test/test_extension_point.py | 80 ++++++++++---------- 9 files changed, 93 insertions(+), 107 deletions(-) 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'}