Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[proposal] introduce dedicated exception for bailing out of plugin functions with a user-customized error message #2656

Open
SnoopJ opened this issue Feb 9, 2025 · 0 comments

Comments

@SnoopJ
Copy link
Contributor

SnoopJ commented Feb 9, 2025

Requested Feature

I propose the addition of a new exception sopel.plugin.PluginAbort or similar that can be used by plugin authors to abort plugin event handling while also sending a fully-custom string error message via IRC.

Prompted by discussion in #sopel on Libera.chat on 9 Feb 2025, with reference to PHP's die() mechanism and sys.exit(), SystemExit in the Python stdlib. Discussion prompted by @half-duplex with participation from @dgw and myself.

Problems Solved

It is relatively common that plugin code wants to abort the handling of a command, event, etc. and it is also common that people¹ solve this problem by doing something like:

from sopel import plugin

@plugin.command("thrust")
def thrust(bot, trigger):
    if cannot_continue_predicate(trigger):
        bot.say("Error: the turbo encabulator is not available")
        return False

    operate_novertrunnion()

This leaves a pitfall where people¹ may forget to terminate control flow:

from sopel import plugin

@plugin.command("thrust")
def thrust(bot, trigger):
    if cannot_continue_predicate(trigger):
        bot.say("Error: the turbo encabulator is not available")
        # NOTE: NO RETURN! Control flow spills out of this block and will probably cause an exception.

    operate_novertrunnion()

I think it is reasonable to say that plugin authors often want to simultaneously express the semantics of "handling has stopped' and "send an error message to IRC". I propose the introduction of a dedicated exception, so that this code can be written as in the following section.

An exception would also make a plugin-level abort more convenient in cases where plugin call stacks get appreciably deep. I.e. the plugin author may detect some fatal error in helper code that does not have direct access to bot and desire a way to propagate an abort all the way to the top-level.


¹ it's me, I'm people

Alternatives

A function plugin.abort() or similar could be introduced to have the same effect. I think this may be a good idea with introduction of an exception, in the same fashion that sys.exit() is a helper that raises SystemExit for you.

@dgw has some reservations about introducing such a helper in plugin so it is possible this has some other home. I argue that plugin is a reasonable place to put it because the proposed feature is very specific to plugin author code, but I can see a valid design concern in keeping the existing suite of decorators separated from other machinery. Perhaps plugin should be a package with a plugin.error and plugin.decorators gathered into the package root to preserve existing API?

@Exirel proposed return bot.say("Custom error message") as an alternative idiom in existing API.

Notes

Implementation notes

In the bot core that handles plugin event dispatch, this feature couple be implemented by doing the moral equivalent of:

try:
    call_handler(bot, trigger)
except PluginAbort as exc:
    # more type-safety/consistency checking is probably a good idea here, contingent on API
    bot.say(exc.args)
except Exception as exc:
    existing_behavior(exc)

Example usage

import random
from sopel import plugin

BARKS = ["arf!", "grrRRR", "yip yip!"]

@plugin.command("bark")
def bark(bot, trigger):
    bot.say(random_bark())


def random_bark() -> str:
    roll = random.randint(1, 4)
    if roll == 4:
        # NOTE: this auxilliary helper does not have access to bot. This abort is not possible without plugin API redesign in the existing Sopel API
        raise plugin.PluginAbort("Cannot locate bark")
    else:
        return random.choice(BARKS)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant