Skip to content

Proposed implementation: event types are concrete subclasses #3653

@aatle

Description

@aatle

From idea #2759

Problem

Pygame events are very generic and give no autocomplete, typing info, or docstring, which places the burden of documentation entirely on the pygame-ce docs instead of on the code.
Event handling can be modernized.

Goals

  • Allow for accurate autocomplete, typing, and documentation when handling events using match statements or isinstance checks
  • Replace integer constant event types, removing the majority of the need for them
  • Fully backwards compatible
  • Added in a version before pygame-ce 3.0
  • Get this event enhancement actually implemented at some point
    • Minimize implementation and review effort by minimizing the scope of the changes

API

Question 1: Is this proposed API, ignoring implementation, acceptable to be planned to be added?

Overview

Event types will each have its own subclass, including built-in event types.
The built-in subclasses have names corresponding to pygame.event.event_name(). Docstrings may be added.
Question 2: Where should these built-in subclasses be defined (new module or event module), and where should they be accessible (top-level pygame or not)?

Every Event class has a class variable type for its specific event type.
(There is a NoEvent subclass, but not a UserEvent subclass because it is a range of types rather than a real type.)

Note: the Event class itself has class variable type = pygame.NOEVENT so that all Event classes have one.
Note: __repr__ for subclasses will give more pythonic representations.

match Statement Usage

These subclasses are suitable for use with match, as they avoid the need for isinstance:

from pygame import event as ev

def process_event(event: Event) -> None:
    match event:
        case ev.MouseMotion(pos=(x, y)):
            ...
        case ev.MouseButtonDown(button=1):
            ...
        case ev.Quit() | ev.KeyDown(key=pg.K_ESCAPE):
            sys.exit()
        case ev.KeyDown(key=pg.K_BACKSPACE, mod=mod):
            end = -1 if mod & pg.KMOD_CTRL == 0 else window.title.rfind(" ") + 1
            window.title = window.title[:end]
        case ev.KeyDown(key=pg.K_RETURN | pg.K_DELETE):
            window.title = ""
        case ev.KeyDown(unicode=char):
            window.title += char
        case ev.Event(type=event_type, dict=attrs):
            print(pg.event.event_name(event_type), attrs)
        case _:
            raise TypeError(event)

Special Event() Constructor

When Event() constructor is called with an event type, it creates an instance of the corresponding subclass for the type, instead of an Event instance, if a subclass exists. (Does not apply to subclass constructors.)
If no subclass exists for that type, then a normal Event with a type instance attribute is returned, as originally.
So, built-in events from pygame.event.get() will be instances of subclasses.

Subclass Constructors

Users can instantiate the events directly with the subclass too.

Suggestion 3: For subclass constructors, they should conventionally be an overridden, normal-looking __init__ that sets attributes with parameters, and does not have **kwargs or arbitrary attributes.
Built-in event subclasses will follow this and (for now) have keyword-only, defaultless parameters:

class KeyDown(Event):  # Definition
    type = KEYDOWN

    def __init__(self, *, unicode, key, mod, scancode, window):
        self.unicode = unicode
        self.key = key
        self.mod = mod
        self.scancode = scancode
        self.window = window

I think this is the best way for a user to define the class such that the constructor gives autocomplete and attribute access gives autocomplete. I excluded arbitrary **kwargs because they don't seem necessary at all.
Question 4: Should certain built-in event attributes like deprecated joy, be given a default value?

The Event class also has a new __init__ overload for no event type, so that user subclasses can use it by default:

    def __init__(self, dict: dict[str, Any] = ..., **kwargs: Any) -> None: ...

On Event itself, it would give a NoEvent.

Suggestion 5: If the default __init__ is used, it is an error to pass in an event type.

Custom Event Types

Users can now also create custom event types by subclassing Event.
By default, the type class variable is automatically created using pygame.event.custom_type().
If type exists already then it is used; if it is invalid, an error is raised. Only unique event types are valid.
Subclassing another subclass is valid.
The __init__ may be overridden, or the user can opt to just use the default Event constructor if they don't care much about typing.

pygame.event.event_types Mapping

A public variable event_types of type types.MappingProxyType[int, type] will map the integer event constants to their corresponding subclass.
This is useful for users to access this information themselves, and is superior to a just a function because of more features.

Stubs

The type of an event is typed as a read-only property (which is compatible with class variables).
The correct attributes for the built-in event type subclasses are added to the stubs.
Arbitrary attributes are still supported with typing with Any using __setattr__ and __getattr__ stubs.

Other Functions

Pygame functions that can take an integer event type as a constant will start also supporting event classes.
Pygame functions that return an integer event type cannot be migrated, but they are not useful with typing anyway.

Implementation

Event and its subclasses will all be implemented in python. Most of the event module will remain in C, for now.
Event will use __new__ for the special constructor behavior, and __init_subclass__ for default custom type and registering subclasses.
The event_types mapping is a mapping proxy of a private _event_types dict.

I don't think there will be any noticeable performance penalties, but this can be tested.

Note: the type of an event instance or class can now technically be changed since there is no way to protect it in the implementation.
Question 6: Does the possibly changing event type pose any extra problems?

PRs

Question 7: Is this implementation plan realistic enough to be implemented, reviewed, and added?

  • The first PR will move the C event module to the internal pygame._event, make pygame.event a python module, and move just the Event class to python. No visible API changes. This seems doable with minimal disruption to the C code.
  • The second PR will implement the new event API on the python side; no C changes should be necessary. Stubs and some docs will be added.
    • In another PR, functions that take integer event types will be updated to also support event subclasses.
  • In another PR, more documentation will be added and updated, with the new API with match statement usage being promoted.
  • Optionally, in other PRs, more of the C event code can be migrated to python such as pygame.event.custom_type(), for better maintainability.

I can do the first and second PRs. Some work will be based off of work in #3121.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions