From b14307270c1438b6f5cdf8de5bb179729a62f993 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 23 Feb 2024 16:53:42 -0500 Subject: [PATCH] Added support for loading extensions This patch adds support for loading extenions to APRSD!! You can create another separate aprsd project, and register your extension in your setup.cfg as a new entry point for aprsd like [entry_points] aprsd.extension = cool = my_project.extension in your my_project/extension.py file import your commmands and away you go. --- aprsd/cli_helper.py | 37 +++++++++++++++++++++++++++++++++++++ aprsd/main.py | 13 +++++++++---- aprsd/utils/__init__.py | 19 +++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index 42c5891..2105a46 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -50,6 +50,43 @@ common_options = [ ] +import click + + +class AliasedGroup(click.Group): + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop('aliases', []) + cmd = click.decorators.command(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop('aliases', []) + cmd = click.decorators.group(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator + + def add_options(options): def _add_options(func): for option in reversed(options): diff --git a/aprsd/main.py b/aprsd/main.py index 68fd9d0..9dbb65f 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -59,20 +59,25 @@ click_completion.core.startswith = custom_startswith click_completion.init() -@click.group(context_settings=CONTEXT_SETTINGS) +@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS) @click.version_option() @click.pass_context def cli(ctx): pass -def main(): - # First import all the possible commands for the CLI - # The commands themselves live in the cmds directory +def load_commands(): from .cmds import ( # noqa completion, dev, fetch_stats, healthcheck, list_plugins, listen, send_message, server, webchat, ) + + +def main(): + # First import all the possible commands for the CLI + # The commands themselves live in the cmds directory + load_commands() + utils.load_entry_points("aprsd.extension") cli(auto_envvar_prefix="APRSD") diff --git a/aprsd/utils/__init__.py b/aprsd/utils/__init__.py index 36d64fd..d0dcb66 100644 --- a/aprsd/utils/__init__.py +++ b/aprsd/utils/__init__.py @@ -4,6 +4,7 @@ import errno import os import re import sys +import traceback import update_checker @@ -131,3 +132,21 @@ def parse_delta_str(s): return {key: float(val) for key, val in m.groupdict().items()} else: return {} + + +def load_entry_points(group): + """Load all extensions registered to the given entry point group""" + print(f"Loading extensions for group {group}") + try: + import importlib_metadata + except ImportError: + # For python 3.10 and later + import importlib.metadata as importlib_metadata + + eps = importlib_metadata.entry_points(group=group) + for ep in eps: + try: + ep.load() + except Exception as e: + print(f"Extension {ep.name} of group {group} failed to load with {e}", file=sys.stderr) + print(traceback.format_exc(), file=sys.stderr)