Skip to content

Commit

Permalink
Deprecate entry_point API in favor of extension_point (#562)
Browse files Browse the repository at this point in the history
This change deprecates the critical entry_point module in colcon-core in
favor of a new module called 'extension_point' which is not coupled to
the pkg_resources module of setuptools like its predecessor.
  • Loading branch information
cottsay committed Aug 17, 2023
1 parent 7b70e61 commit 93b58ce
Show file tree
Hide file tree
Showing 26 changed files with 448 additions and 91 deletions.
26 changes: 14 additions & 12 deletions bin/colcon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions colcon_core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
6 changes: 6 additions & 0 deletions colcon_core/entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
155 changes: 155 additions & 0 deletions colcon_core/extension_point.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 3 additions & 3 deletions colcon_core/plugin_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions colcon_core/task/python/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions test/extension_point_context.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apache
argcomplete
argparse
asyncio
attrs
autouse
basepath
bazqux
Expand Down
6 changes: 3 additions & 3 deletions test/test_argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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())
Expand All @@ -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
Expand Down
Loading

0 comments on commit 93b58ce

Please sign in to comment.