Skip to content

feat: support custom Python events with optional cancellation#368

Merged
wu-vincent merged 4 commits intomainfrom
feat/custom-python-events
Apr 7, 2026
Merged

feat: support custom Python events with optional cancellation#368
wu-vincent merged 4 commits intomainfrom
feat/custom-python-events

Conversation

@wu-vincent
Copy link
Copy Markdown
Member

@wu-vincent wu-vincent commented Apr 7, 2026

Summary

  • Add PyEvent trampoline class (event.h) enabling Python subclasses of Event to work through pybind11 — previously custom events were not possible in the Python API
  • Move Cancellable to a pure Python mixin so custom events can opt into cancellation via class MyEvent(Event, Cancellable) multiple inheritance
  • Remove deprecated cancelled property (replaced by is_cancelled in a prior release)
  • Fix event name resolution — built-in events use the short class name, custom plugin events use fully qualified module.qualname to avoid collisions
  • Fix EventHandler dispatch — use virtual isCancelled() on Event so cancellation state is read correctly for both C++ and Python events
  • Add unit tests (tests/test_event.py) and runtime integration tests (tests/endstone_test/.../test_custom_event.py)

Example

from endstone.event import Cancellable, Event, EventPriority, event_handler
from endstone.plugin import Plugin


# Define a simple custom event
class MyCustomEvent(Event):
    def __init__(self, message: str):
        super().__init__()
        self.message = message


# Define a cancellable custom event
class MyAnnouncement(Event, Cancellable):
    def __init__(self, text: str):
        super().__init__()
        self.text = text


class MyPlugin(Plugin):
    def on_enable(self):
        self.register_events(self)

    # Listen for the custom event
    @event_handler
    def on_custom(self, event: MyCustomEvent):
        self.logger.info(f"Received: {event.message}")

    # Listen with priority, skip if cancelled
    @event_handler(priority=EventPriority.HIGH, ignore_cancelled=True)
    def on_announcement(self, event: MyAnnouncement):
        self.server.broadcast_message(event.text)

    def some_method(self):
        # Fire the events
        self.server.plugin_manager.call_event(MyCustomEvent("hello"))

        announcement = MyAnnouncement("Server restarting soon!")
        self.server.plugin_manager.call_event(announcement)
        if not announcement.is_cancelled:
            # proceed with default behavior
            ...

Design

  • C++ events continue to use Cancellable<T> template + ICancellable pybind11 binding
  • Python custom events use PyEvent as the trampoline, which implements ICancellable by reading/writing the _cancelled attribute on the Python object — consistent with the Python Cancellable mixin
  • PyEvent::isCancellable() checks at runtime whether the Python class inherits from the Python Cancellable mixin
  • Event now has a private virtual isCancelled() so EventHandler dispatches correctly for both C++ (reads cancelled_ field) and Python events (reads Python attribute) via vtable

Test plan

  • pytest tests/test_event.py — unit tests for custom event name, async flag, cancellation, un-cancellation, and async+cancellable
  • Runtime integration tests (test_custom_event.py) — fire/handle cycle through plugin manager, cancellation priority semantics
  • Existing event tests still pass
  • C++ tests (ctest) unaffected

🤖 Generated with Claude Code

Previously, Python plugins could not define custom events — only C++
events exposed via pybind11 were available. This adds a PyEvent
trampoline class that allows Python subclasses of Event to work
correctly, and moves Cancellable to a pure Python mixin so custom
events can opt into cancellation via multiple inheritance.

Also removes the deprecated `cancelled` property (replaced by
`is_cancelled` in a prior release).
register_events and PyEvent::getEventName() now agree on naming:
built-in events (endstone.*) use the short class name, custom plugin
events use the fully qualified module.qualname to avoid collisions.

Also adds runtime integration tests for custom event fire/handle cycle
including cancellation semantics via plugin manager.
EventHandler::callEvent() was reading the C++ cancelled_ field directly,
but custom Python events store cancellation state in a Python attribute.
Use virtual dispatch so PyEvent::isCancelled() is called correctly.
@wu-vincent wu-vincent merged commit a98d70a into main Apr 7, 2026
2 of 3 checks passed
@wu-vincent wu-vincent deleted the feat/custom-python-events branch April 7, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant