diff --git a/bin/colcon b/bin/colcon index 8e228042..f543142c 100755 --- a/bin/colcon +++ b/bin/colcon @@ -18,28 +18,28 @@ sys.path.insert(0, pkg_root) # override entry point discovery -from colcon_core import entry_point # noqa: E402 +from colcon_core import extension_point # noqa: E402 -custom_entry_points = {} +custom_extension_points = {} -def custom_load_entry_points(group_name, *, exclude_names=None): # noqa: D103 - global custom_entry_points - assert group_name in custom_entry_points, \ - f"get_entry_points() not overridden for group '{group_name}'" +def custom_load_extension_points( # noqa: D103 + group_name, *, excludes=None +): + global custom_extension_points + assert group_name in custom_extension_points, \ + f"get_extension_points() not overridden for group '{group_name}'" return { - k: v for k, v in custom_entry_points[group_name].items() - if exclude_names is None or k not in exclude_names} + k: v for k, v in custom_extension_points[group_name].items() + if excludes is None or k not in excludes} # override function before importing other modules -entry_point.load_entry_points = custom_load_entry_points +extension_point.load_extension_points = custom_load_extension_points from colcon_core.command import HOME_ENVIRONMENT_VARIABLE # noqa: E402 I202 from colcon_core.command import LOG_LEVEL_ENVIRONMENT_VARIABLE # noqa: E402 -from colcon_core.entry_point \ - import EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE # noqa: E402 from colcon_core.environment.path import PathEnvironment # noqa: E402 from colcon_core.environment.path \ import PythonScriptsPathEnvironment # noqa: E402 @@ -54,6 +54,8 @@ from colcon_core.event_handler.log_command \ from colcon_core.executor \ import DEFAULT_EXECUTOR_ENVIRONMENT_VARIABLE # noqa: E402 from colcon_core.executor.sequential import SequentialExecutor # noqa: E402 +from colcon_core.extension_point \ + import EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE # noqa: E402 from colcon_core.package_augmentation.python \ import PythonPackageAugmentation # noqa: E402 from colcon_core.package_discovery.path \ @@ -77,7 +79,7 @@ from colcon_core.verb.build import BuildVerb # noqa: E402 from colcon_core.verb.test import TestVerb # noqa: E402 -custom_entry_points.update({ +custom_extension_points.update({ 'colcon_core.argument_parser': {}, 'colcon_core.environment': { 'path': PathEnvironment, diff --git a/colcon_core/command.py b/colcon_core/command.py index e5cd6a08..07dddaf1 100644 --- a/colcon_core/command.py +++ b/colcon_core/command.py @@ -51,7 +51,7 @@ from colcon_core.argument_parser import decorate_argument_parser # noqa: E402 E501 I100 I202 from colcon_core.argument_parser import SuppressUsageOutput # noqa: E402 -from colcon_core.entry_point import load_entry_points # noqa: E402 +from colcon_core.extension_point import load_extension_points # noqa: E402 from colcon_core.location import create_log_path # noqa: E402 from colcon_core.location import get_log_path # noqa: E402 from colcon_core.location import set_default_config_path # noqa: E402 @@ -286,7 +286,7 @@ def get_environment_variables_epilog(group_name): :rtype: str """ # list environment variables with descriptions - entry_points = load_entry_points(group_name) + entry_points = load_extension_points(group_name) env_vars = { env_var.name: env_var.description for env_var in entry_points.values()} epilog_lines = [] diff --git a/colcon_core/entry_point.py b/colcon_core/entry_point.py index d290b3bb..2bec5d4f 100644 --- a/colcon_core/entry_point.py +++ b/colcon_core/entry_point.py @@ -17,6 +17,12 @@ 'COLCON_EXTENSION_BLOCKLIST', 'Block extensions which should not be used') +# See colcon/colcon-core#562 +warnings.warn( + "'colcon_core.entry_point' has been deprecated, " + "use 'colcon_core.extension_point' instead", + stacklevel=2) + if sys.version_info[:2] >= (3, 7): def __getattr__(name): global EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE diff --git a/colcon_core/extension_point.py b/colcon_core/extension_point.py new file mode 100644 index 00000000..54191e7a --- /dev/null +++ b/colcon_core/extension_point.py @@ -0,0 +1,155 @@ +# Copyright 2016-2018 Dirk Thomas +# Copyright 2023 Open Source Robotics Foundation, Inc. +# Licensed under the Apache License, Version 2.0 + +from collections import defaultdict +import os +import traceback + +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( + 'COLCON_EXTENSION_BLOCKLIST', + 'Block extensions which should not be used') + +logger = colcon_logger.getChild(__name__) + + +""" +The group name for entry points identifying colcon extension points. + +While all entry points in this package start with `colcon_core.` other +distributions might define entry points with a different prefix. +Those need to be declared using this group name. +""" +EXTENSION_POINT_GROUP_NAME = 'colcon_core.extension_point' + + +def get_all_extension_points(): + """ + Get all extension points related to `colcon` and any of its extensions. + + :returns: mapping of extension point groups to dictionaries which map + extension point names to a tuple of extension point values, dist name, + and dist version + :rtype: dict + """ + global EXTENSION_POINT_GROUP_NAME + colcon_extension_points = get_extension_points(EXTENSION_POINT_GROUP_NAME) + 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(): + # skip groups which are not registered as extension points + if group_name 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)) + return entry_points + + +def get_extension_points(group): + """ + Get the extension points for a specific group. + + :param str group: the name of the extension point 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] + logger.error( + f"Entry point '{group}.{entry_point.name}' is declared " + f"multiple times, '{entry_point}' 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 + + +def load_extension_points(group, *, excludes=None): + """ + Load the extension points for a specific group. + + :param str group: the name of the extension point group + :param iterable excludes: the names of the extension points to exclude + :returns: mapping of entry point names to loaded entry points + :rtype: dict + """ + extension_types = {} + for name, value in get_extension_points(group).items(): + if excludes and name in excludes: + continue + try: + extension_type = load_extension_point(name, value, group) + except RuntimeError: + continue + except Exception as e: # noqa: F841 + # catch exceptions raised when loading entry point + exc = traceback.format_exc() + logger.error( + 'Exception loading extension ' + f"'{group}.{name}': {e}\n{exc}") + # skip failing entry point, continue with next one + continue + extension_types[name] = extension_type + return extension_types + + +def load_extension_point(name, value, group): + """ + Load the extension point. + + :param name: the name of the extension entry point. + :param value: the value of the extension entry point. + :param group: the name of the group the extension entry point is a part of. + :returns: the loaded entry point + :raises RuntimeError: if either the group name or the entry point name is + listed in the environment variable + :const:`EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE` + """ + global EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE + blocklist = os.environ.get( + EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name, None) + if blocklist: + blocklist = blocklist.split(os.pathsep) + if group in blocklist: + raise RuntimeError( + 'The entry point group name is listed in the environment ' + f"variable '{EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE.name}'") + full_name = f'{group}.{name}' + if full_name in blocklist: + 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() diff --git a/colcon_core/plugin_system.py b/colcon_core/plugin_system.py index dc9df955..7d3934a3 100644 --- a/colcon_core/plugin_system.py +++ b/colcon_core/plugin_system.py @@ -4,7 +4,7 @@ from collections import OrderedDict import traceback -from colcon_core.entry_point import load_entry_points +from colcon_core.extension_point import load_extension_points from colcon_core.logging import colcon_logger from packaging.version import Version @@ -33,8 +33,8 @@ def instantiate_extensions( instantiated even when it has been created and cached before :returns: dict of extensions """ - extension_types = load_entry_points( - group_name, exclude_names=exclude_names) + extension_types = load_extension_points( + group_name, excludes=exclude_names) extension_instances = {} for extension_name, extension_class in extension_types.items(): extension_instance = _instantiate_extension( diff --git a/colcon_core/task/python/test/__init__.py b/colcon_core/task/python/test/__init__.py index fe21ffea..ef5298bc 100644 --- a/colcon_core/task/python/test/__init__.py +++ b/colcon_core/task/python/test/__init__.py @@ -4,7 +4,7 @@ import re import traceback -from colcon_core.entry_point import load_entry_points +from colcon_core.extension_point import load_extension_points from colcon_core.logging import colcon_logger from colcon_core.package_augmentation.python import extract_dependencies from colcon_core.plugin_system import get_first_line_doc @@ -194,7 +194,7 @@ def get_python_testing_step_extension(step_name): :returns: A unique instance of the extension, otherwise None """ group_name = 'colcon_core.python_testing' - extension_types = load_entry_points(group_name) + extension_types = load_extension_points(group_name) extension_names = list(extension_types.keys()) if step_name not in extension_names: return None diff --git a/setup.cfg b/setup.cfg index f1ce310f..ff472b2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,7 +88,7 @@ colcon_core.environment = colcon_core.environment_variable = all_shells = colcon_core.shell:ALL_SHELLS_ENVIRONMENT_VARIABLE default_executor = colcon_core.executor:DEFAULT_EXECUTOR_ENVIRONMENT_VARIABLE - extension_blocklist = colcon_core.entry_point:EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE + extension_blocklist = colcon_core.extension_point:EXTENSION_BLOCKLIST_ENVIRONMENT_VARIABLE home = colcon_core.command:HOME_ENVIRONMENT_VARIABLE log_level = colcon_core.command:LOG_LEVEL_ENVIRONMENT_VARIABLE warnings = colcon_core.command:WARNINGS_ENVIRONMENT_VARIABLE diff --git a/test/extension_point_context.py b/test/extension_point_context.py new file mode 100644 index 00000000..0b2fb712 --- /dev/null +++ b/test/extension_point_context.py @@ -0,0 +1,29 @@ +# Copyright 2016-2018 Dirk Thomas +# Copyright 2023 Open Source Robotics Foundation, Inc. +# Licensed under the Apache License, Version 2.0 + +from colcon_core import plugin_system + + +class ExtensionPointContext: + + def __init__(self, **kwargs): + self._kwargs = kwargs + self._memento = None + + def __enter__(self): + # reset entry point cache, provide new instances in each scope + plugin_system._extension_instances.clear() + + self._memento = plugin_system.load_extension_points + + def load_extension_points(_, *, excludes=None): + nonlocal self + return { + k: v for k, v in self._kwargs.items() + if excludes is None or k not in excludes} + + plugin_system.load_extension_points = load_extension_points + + def __exit__(self, *_): + plugin_system.load_extension_points = self._memento diff --git a/test/spell_check.words b/test/spell_check.words index 4d060a0a..820f1af3 100644 --- a/test/spell_check.words +++ b/test/spell_check.words @@ -3,6 +3,7 @@ apache argcomplete argparse asyncio +attrs autouse basepath bazqux diff --git a/test/test_argument_parser.py b/test/test_argument_parser.py index 95165fd8..ba35c425 100644 --- a/test/test_argument_parser.py +++ b/test/test_argument_parser.py @@ -11,7 +11,7 @@ from colcon_core.argument_parser import get_argument_parser_extensions import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(ArgumentParserDecoratorExtensionPoint): @@ -23,7 +23,7 @@ class Extension2(ArgumentParserDecoratorExtensionPoint): def test_get_argument_parser_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_argument_parser_extensions() assert ['extension2', 'extension1'] == \ list(extensions.keys()) @@ -42,7 +42,7 @@ def add_argument(self, *args, **kwargs): def test_decorate_argument_parser(): parser = ArgumentParser() - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_argument_parser_extensions() # one invalid return value, one not implemented diff --git a/test/test_command.py b/test/test_command.py index 0c350138..cdd54770 100644 --- a/test/test_command.py +++ b/test/test_command.py @@ -16,7 +16,7 @@ from colcon_core.verb import VerbExtensionPoint import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(VerbExtensionPoint): @@ -37,7 +37,7 @@ def add_arguments(self, *, parser): def test_main(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3 ): with patch( @@ -59,7 +59,7 @@ def test_main(): main(argv=argv + ['--log-level', 'info']) with patch( - 'colcon_core.command.load_entry_points', + 'colcon_core.command.load_extension_points', return_value={ 'key1': EnvironmentVariable('name', 'description'), 'key2': EnvironmentVariable( @@ -83,7 +83,7 @@ def test_main(): def test_create_parser(): - with EntryPointContext(): + with ExtensionPointContext(): parser = create_parser('colcon_core.environment_variable') parser.add_argument('--foo', nargs='*', type=str.lstrip) @@ -100,7 +100,7 @@ def test_create_parser(): argv = sys.argv sys.argv = ['/some/path/prog_name/__main__.py'] + sys.argv[1:] - with EntryPointContext(): + with ExtensionPointContext(): parser = create_parser('colcon_core.environment_variable') sys.argv = argv assert parser.prog == 'prog_name' diff --git a/test/test_entry_point.py b/test/test_entry_point.py index f632480e..bbb797b1 100644 --- a/test/test_entry_point.py +++ b/test/test_entry_point.py @@ -4,12 +4,18 @@ import os from unittest.mock import Mock from unittest.mock import patch +import warnings + +with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', message='.*entry_point.*deprecated.*', category=UserWarning) + + from colcon_core.entry_point import EXTENSION_POINT_GROUP_NAME + from colcon_core.entry_point import get_all_entry_points + from colcon_core.entry_point import get_entry_points + from colcon_core.entry_point import load_entry_point + from colcon_core.entry_point import load_entry_points -from colcon_core.entry_point import EXTENSION_POINT_GROUP_NAME -from colcon_core.entry_point import get_all_entry_points -from colcon_core.entry_point import get_entry_points -from colcon_core.entry_point import load_entry_point -from colcon_core.entry_point import load_entry_points import pytest from .environment_context import EnvironmentContext diff --git a/test/test_environment.py b/test/test_environment.py index 1eaed5cd..2de07892 100644 --- a/test/test_environment.py +++ b/test/test_environment.py @@ -15,7 +15,7 @@ from colcon_core.shell import ShellExtensionPoint import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext def test_extension_interface(): @@ -38,7 +38,7 @@ class Extension2(EnvironmentExtensionPoint): def test_get_environment_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_environment_extensions() assert list(extensions.keys()) == ['extension2', 'extension1'] @@ -70,7 +70,9 @@ def test_create_environment_scripts(): create_environment_scripts(pkg, args) pkg.hooks = [os.path.join(basepath, 'subA')] - with EntryPointContext(extension3=Extension3, extension4=Extension4): + with ExtensionPointContext( + extension3=Extension3, extension4=Extension4 + ): extensions = get_shell_extensions() # one invalid return value, one check correct hooks argument extensions[100]['extension3'].create_package_script = Mock() @@ -101,7 +103,9 @@ def test_create_environment_scripts(): def test_create_environment_hooks(): with TemporaryDirectory(prefix='test_colcon_') as basepath: - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext( + extension1=Extension1, extension2=Extension2 + ): with patch('colcon_core.environment.logger.error') as error: hooks = create_environment_hooks(basepath, 'pkg_name') assert len(hooks) == 2 diff --git a/test/test_event_handler.py b/test/test_event_handler.py index 873c957a..1df8f561 100644 --- a/test/test_event_handler.py +++ b/test/test_event_handler.py @@ -11,7 +11,7 @@ from colcon_core.event_handler import get_event_handler_extensions import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(EventHandlerExtensionPoint): @@ -39,7 +39,7 @@ def test_extension_interface(): def test_get_shell_extensions(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3 ): extensions = get_event_handler_extensions(context=None) @@ -49,7 +49,7 @@ def test_get_shell_extensions(): def test_add_event_handler_arguments(): parser = argparse.ArgumentParser() - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3 ): add_event_handler_arguments(parser) @@ -62,7 +62,7 @@ def test_add_event_handler_arguments(): def test_apply_event_handler_arguments(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, ): extensions = get_event_handler_extensions(context=None) diff --git a/test/test_event_reactor.py b/test/test_event_reactor.py index 81c62f81..fd29df0c 100644 --- a/test/test_event_reactor.py +++ b/test/test_event_reactor.py @@ -11,7 +11,7 @@ from colcon_core.event_reactor import create_event_reactor from colcon_core.event_reactor import EventReactorShutdown -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class CustomExtension(EventHandlerExtensionPoint): @@ -49,7 +49,7 @@ def test_create_event_reactor(): context = Mock() context.args = Mock() context.args.event_handlers = [] - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3 ): event_reactor = create_event_reactor(context) diff --git a/test/test_executor.py b/test/test_executor.py index 49847e40..80c2f812 100644 --- a/test/test_executor.py +++ b/test/test_executor.py @@ -21,8 +21,8 @@ from colcon_core.subprocess import SIGINT_RESULT import pytest -from .entry_point_context import EntryPointContext from .environment_context import EnvironmentContext +from .extension_point_context import ExtensionPointContext from .run_until_complete import run_until_complete @@ -143,7 +143,7 @@ class Extension3(ExecutorExtensionPoint): def test_add_executor_arguments(): parser = ArgumentParser() # extensions with the same priority - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3 ): with pytest.raises(AssertionError) as e: @@ -152,13 +152,13 @@ def test_add_executor_arguments(): str(e.value) # no extensions - with EntryPointContext(): + with ExtensionPointContext(): with pytest.raises(AssertionError) as e: add_executor_arguments(parser) assert 'No executor extensions found' in str(e.value) # choose executor by environment variable - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_executor_extensions() extensions[110]['extension2'].add_arguments = Mock( side_effect=RuntimeError('custom exception')) @@ -178,7 +178,7 @@ def test_add_executor_arguments(): # choose default executor parser = ArgumentParser() - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): add_executor_arguments(parser) args = parser.parse_args([]) assert args.executor == 'extension2' @@ -202,7 +202,9 @@ def test_execute_jobs(): with patch( 'colcon_core.executor.create_event_reactor', return_value=event_reactor ): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext( + extension1=Extension1, extension2=Extension2 + ): # no extension selected with pytest.raises(AssertionError): execute_jobs(context, jobs) diff --git a/test/test_extension_point.py b/test/test_extension_point.py new file mode 100644 index 00000000..73b6a2d4 --- /dev/null +++ b/test/test_extension_point.py @@ -0,0 +1,148 @@ +# Copyright 2016-2018 Dirk Thomas +# Copyright 2023 Open Source Robotics Foundation, Inc. +# Licensed under the Apache License, Version 2.0 + +import os +from unittest.mock import DEFAULT +from unittest.mock import patch + +from colcon_core.extension_point import EntryPoint +from colcon_core.extension_point import EXTENSION_POINT_GROUP_NAME +from colcon_core.extension_point import get_all_extension_points +from colcon_core.extension_point import get_extension_points +from colcon_core.extension_point import load_extension_point +from colcon_core.extension_point import load_extension_points +import pytest + +from .environment_context import EnvironmentContext + + +Group1 = EntryPoint('group1', 'g1') +Group2 = EntryPoint('group2', 'g2') + + +class Dist(): + + project_name = 'dist' + + def __init__(self, group_name, group): + self._group_name = group_name + self._group = group + + def __lt__(self, other): + return self._group_name < other._group_name + + def get_entry_map(self): + return self._group + + +def iter_entry_points(*, group): + if group == EXTENSION_POINT_GROUP_NAME: + return [Group1, Group2] + assert group == Group1.name + ep1 = EntryPoint('extA', 'eA') + ep2 = EntryPoint('extB', 'eB') + return [ep1, ep2] + + +def working_set(): + 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')}}), + ] + + +def test_all_extension_points(): + with patch( + 'colcon_core.extension_point.iter_entry_points', + side_effect=iter_entry_points + ): + with patch( + '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'] == ( + 'eA', Dist.project_name, None) + + +def test_extension_point_blocklist(): + # successful loading of extension point without a blocklist + with patch( + 'colcon_core.extension_point.iter_entry_points', + side_effect=iter_entry_points + ): + with patch( + '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, 'resolve', return_value=None) as resolve: + load_extension_point('extA', 'eA', 'group1') + assert resolve.call_count == 1 + + # successful loading of entry point not in blocklist + resolve.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 + + # entry point in a blocked group can't be loaded + 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 resolve.call_count == 0 + + # entry point listed in the blocklist can't be loaded + with EnvironmentContext(COLCON_EXTENSION_BLOCKLIST=os.pathsep.join([ + 'group1.extA', 'group1.extB']) + ): + with pytest.raises(RuntimeError) as e: + 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 + + +def entry_point_resolve(self, *args, **kwargs): + if self.name == 'exception': + raise Exception('entry point raising exception') + if self.name == 'runtime_error': + raise RuntimeError('entry point raising runtime error') + elif self.name == 'success': + return + return DEFAULT + + +@patch.object(EntryPoint, 'resolve', entry_point_resolve) +@patch( + 'colcon_core.extension_point.get_extension_points', + return_value={'exception': 'a', 'runtime_error': 'b', 'success': 'c'} +) +def test_load_extension_points_with_exception(_): + with patch('colcon_core.extension_point.logger.error') as error: + extensions = load_extension_points('group') + # the extension point raising an exception different than a runtime error + # results in an error message + assert error.call_count == 1 + assert len(error.call_args[0]) == 1 + assert "Exception loading extension 'group.exception'" \ + in error.call_args[0][0] + assert 'entry point raising exception' in error.call_args[0][0] + # neither of the extension points was loaded successfully + assert extensions == {'success': None} diff --git a/test/test_package_augmentation.py b/test/test_package_augmentation.py index 5e769f09..aeea1d5e 100644 --- a/test/test_package_augmentation.py +++ b/test/test_package_augmentation.py @@ -13,7 +13,7 @@ from colcon_core.package_augmentation import update_metadata from colcon_core.package_descriptor import PackageDescriptor -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(PackageAugmentationExtensionPoint): @@ -25,7 +25,7 @@ class Extension2(PackageAugmentationExtensionPoint): def test_get_package_augmentation_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_augmentation_extensions() assert ['extension2', 'extension1'] == \ list(extensions.keys()) @@ -48,7 +48,7 @@ def test_augment_packages(): desc1 = PackageDescriptor('/some/path') desc2 = PackageDescriptor('/other/path') descs = {desc1, desc2} - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_augmentation_extensions() extensions['extension1'].augment_package = Mock( side_effect=augment_package_metadata_with_data) @@ -67,7 +67,7 @@ def test_augment_packages(): desc1 = PackageDescriptor('/some/path') desc2 = PackageDescriptor('/other/path') descs = {desc1, desc2} - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_augmentation_extensions() extensions['extension1'].augment_package = Mock( side_effect=augment_package_with_hook) diff --git a/test/test_package_discovery.py b/test/test_package_discovery.py index f55493b2..1533ac25 100644 --- a/test/test_package_discovery.py +++ b/test/test_package_discovery.py @@ -16,7 +16,7 @@ from colcon_core.package_discovery import get_package_discovery_extensions from colcon_core.package_discovery import PackageDiscoveryExtensionPoint -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(PackageDiscoveryExtensionPoint): @@ -36,7 +36,7 @@ class Extension4(PackageDiscoveryExtensionPoint): def test_get_package_discovery_extensions(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): @@ -47,7 +47,7 @@ def test_get_package_discovery_extensions(): def test_add_package_discovery_arguments(): parser = Mock() - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): @@ -98,7 +98,7 @@ def test_discover_packages(): warn.assert_called_once_with('No package discovery extensions found') assert descs == set() - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): @@ -152,7 +152,7 @@ def test_expand_dir_wildcards(): def test__get_extensions_with_parameters(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): @@ -178,7 +178,7 @@ def test__discover_packages(): descs = _discover_packages(None, None, {}) assert descs == set() - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): diff --git a/test/test_package_identification.py b/test/test_package_identification.py index 5ea932d1..416bcfca 100644 --- a/test/test_package_identification.py +++ b/test/test_package_identification.py @@ -15,7 +15,7 @@ import PackageIdentificationExtensionPoint import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(PackageIdentificationExtensionPoint): @@ -35,7 +35,7 @@ class Extension4(PackageIdentificationExtensionPoint): def test_get_package_identification_extensions(): - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): @@ -61,7 +61,7 @@ def identify_name_and_type(desc): def test_identify(): path = '/some/path' - context = EntryPointContext( + context = ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4) @@ -118,7 +118,7 @@ def test_identify(): def test__identify(): desc_path_only = PackageDescriptor('/some/path') - with EntryPointContext( + with ExtensionPointContext( extension1=Extension1, extension2=Extension2, extension3=Extension3, extension4=Extension4, ): diff --git a/test/test_package_selection.py b/test/test_package_selection.py index ff393922..1ee08bfa 100644 --- a/test/test_package_selection.py +++ b/test/test_package_selection.py @@ -16,7 +16,7 @@ from colcon_core.package_selection import select_package_decorators import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext class Extension1(PackageSelectionExtensionPoint): @@ -28,7 +28,7 @@ class Extension2(PackageSelectionExtensionPoint): def test_get_package_selection_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_selection_extensions() assert ['extension1', 'extension2'] == list(extensions.keys()) @@ -39,7 +39,7 @@ def add_dummy_arguments(parser): def test__add_package_selection_arguments(): parser = Mock() - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_selection_extensions() # invalid return value @@ -108,7 +108,7 @@ def test__check_package_selection_parameters(): args = Mock() pkg_names = Mock() - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_selection_extensions() # nothing wrong with the arguments @@ -161,7 +161,7 @@ def test_select_package_decorators(): deco2.selected = True decos = [deco1, deco2] - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_package_selection_extensions() # raise exception diff --git a/test/test_plugin_system.py b/test/test_plugin_system.py index 984fd218..e075c207 100644 --- a/test/test_plugin_system.py +++ b/test/test_plugin_system.py @@ -12,7 +12,7 @@ from colcon_core.plugin_system import SkipExtensionException import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext def test_instantiate_extensions(): @@ -22,7 +22,7 @@ class Extension1: class Extension2: pass - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): # successful instantiation of extensions extensions = instantiate_extensions('group') assert 'extension1' in extensions.keys() @@ -56,7 +56,7 @@ def __init__(self): raise SkipExtensionException( 'extension raising skip extension exception') - with EntryPointContext( + with ExtensionPointContext( exception=ExtensionRaisingException, skip_extension_exception=ExtensionSkipExtensionException ): @@ -97,7 +97,7 @@ class ExtensionC: def test_order_extensions_by_name(): - with EntryPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): + with ExtensionPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): extensions = instantiate_extensions('group') # ensure correct order based on name ordered_extensions = order_extensions_by_name(extensions) @@ -105,7 +105,7 @@ def test_order_extensions_by_name(): def test_order_extensions_by_priority(): - with EntryPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): + with ExtensionPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): extensions = instantiate_extensions('group') # ensure correct order based on priority ordered_extensions = order_extensions_by_priority(extensions) @@ -113,7 +113,7 @@ def test_order_extensions_by_priority(): def test_order_extensions_grouped_by_priority(): - with EntryPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): + with ExtensionPointContext(foo=ExtensionA, bar=ExtensionB, baz=ExtensionC): extensions = instantiate_extensions('group') # ensure correct order based on priority grouped_extensions = order_extensions_grouped_by_priority(extensions) diff --git a/test/test_prefix_path.py b/test/test_prefix_path.py index fc6fce6c..35a1f356 100644 --- a/test/test_prefix_path.py +++ b/test/test_prefix_path.py @@ -13,8 +13,8 @@ from colcon_core.prefix_path.colcon import ColconPrefixPath import pytest -from .entry_point_context import EntryPointContext from .environment_context import EnvironmentContext +from .extension_point_context import ExtensionPointContext class Extension1(PrefixPathExtensionPoint): @@ -32,7 +32,7 @@ def test_extension_interface(): def test_get_prefix_path_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_prefix_path_extensions() assert list(extensions.keys()) == [100, 90] assert list(extensions[100].keys()) == ['extension2'] @@ -92,7 +92,7 @@ def test_get_chained_prefix_path(): assert prefix_path == [str(basepath)] assert warn.call_count == 0 - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_prefix_path_extensions() # one invalid return value, one not implemented diff --git a/test/test_shell.py b/test/test_shell.py index aeef09cb..0c0378fb 100644 --- a/test/test_shell.py +++ b/test/test_shell.py @@ -25,8 +25,8 @@ from colcon_core.shell.installed_packages import MergedInstalledPackageFinder import pytest -from .entry_point_context import EntryPointContext from .environment_context import EnvironmentContext +from .extension_point_context import ExtensionPointContext from .run_until_complete import run_until_complete @@ -59,7 +59,7 @@ def test_extension_interface(): def test_get_shell_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_shell_extensions() assert list(extensions.keys()) == [100, 90] assert list(extensions[100].keys()) == ['extension2'] @@ -71,7 +71,7 @@ async def generate_command_environment(task_name, build_base, dependencies): def test_get_command_environment(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_shell_extensions() # one not implemented, one skipped extension @@ -169,7 +169,7 @@ class Extension5(ShellExtensionPoint): def test_create_environment_hook(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): # no primary shell extension with pytest.raises(RuntimeError) as e: create_environment_hook(None, None, None, None, None) @@ -177,7 +177,7 @@ def test_create_environment_hook(): 'Could not find a primary shell extension for creating an ' 'environment hook') - with EntryPointContext( + with ExtensionPointContext( extension3=Extension3, extension4=Extension4, extension5=Extension5 ): extensions = get_shell_extensions() @@ -327,7 +327,9 @@ class FIExtension2(FindInstalledPackagesExtensionPoint): def test_get_find_installed_packages_extensions(): - with EntryPointContext(extension1=FIExtension1, extension2=FIExtension2): + with ExtensionPointContext( + extension1=FIExtension1, extension2=FIExtension2 + ): extensions = get_find_installed_packages_extensions() assert list(extensions.keys()) == [100, 90] assert list(extensions[100].keys()) == ['extension2'] @@ -366,7 +368,7 @@ def test_find_installed_packages_in_environment(): def test_find_installed_packages(): - with EntryPointContext( + with ExtensionPointContext( colcon_isolated=IsolatedInstalledPackageFinder, colcon_merged=MergedInstalledPackageFinder ): @@ -427,7 +429,7 @@ def find_installed_packages(self, install_base: Path): def test_inconsistent_package_finding_extensions(): - with EntryPointContext(dne=FIExtensionPathNotExist): + with ExtensionPointContext(dne=FIExtensionPathNotExist): with TemporaryDirectory(prefix='test_colcon_') as install_base: install_base = Path(install_base) with patch('colcon_core.shell.logger.warning') as mock_warn: @@ -456,7 +458,9 @@ class PackageLocation2(FindInstalledPackagesExtensionPoint): def find_installed_packages(self, base: Path): return {'pkgA': location2} - with EntryPointContext(loc1=PackageLocation1, loc2=PackageLocation2): + with ExtensionPointContext( + loc1=PackageLocation1, loc2=PackageLocation2 + ): with patch('colcon_core.shell.logger.warning') as mock_warn: assert {'pkgA': location1} == find_installed_packages(base) mock_warn.assert_called_once_with( diff --git a/test/test_task.py b/test/test_task.py index 3d1ad48c..c0e940fc 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -23,7 +23,7 @@ from colcon_core.task import TaskExtensionPoint import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext from .run_until_complete import run_until_complete @@ -123,7 +123,7 @@ def instantiate_extensions_without_cache( def test_add_task_arguments(): parser = Mock() task_name = 'colcon_core.task.build' - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): with patch( 'colcon_core.task.instantiate_extensions', side_effect=instantiate_extensions_without_cache @@ -155,7 +155,7 @@ def test_add_task_arguments(): def test_get_task_extension(): task_name = 'colcon_core.task.build' - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): # request invalid extension extension = get_task_extension(task_name, 'package_type') assert extension is None diff --git a/test/test_verb.py b/test/test_verb.py index f50bd5bb..ee041e55 100644 --- a/test/test_verb.py +++ b/test/test_verb.py @@ -14,7 +14,7 @@ from colcon_core.verb import VerbExtensionPoint import pytest -from .entry_point_context import EntryPointContext +from .extension_point_context import ExtensionPointContext def test_verb_interface(): @@ -34,7 +34,7 @@ class Extension2(VerbExtensionPoint): def test_get_verb_extensions(): - with EntryPointContext(extension1=Extension1, extension2=Extension2): + with ExtensionPointContext(extension1=Extension1, extension2=Extension2): extensions = get_verb_extensions() assert list(extensions.keys()) == ['extension1', 'extension2']