Skip to content

Commit f42c049

Browse files
authored
Typed enums (#112)
1 parent 7429f0b commit f42c049

File tree

6 files changed

+199
-150
lines changed

6 files changed

+199
-150
lines changed

rendercanvas/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
from ._version import __version__, version_info
88
from . import _coreutils
9-
from ._events import EventType
10-
from ._scheduler import UpdateMode
11-
from .base import BaseRenderCanvas, BaseLoop, CursorShape
9+
from ._enums import CursorShape, EventType, UpdateMode
10+
from .base import BaseRenderCanvas, BaseLoop
1211

1312
__all__ = ["BaseLoop", "BaseRenderCanvas", "CursorShape", "EventType", "UpdateMode"]

rendercanvas/_coreutils.py

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import re
77
import sys
8-
import types
98
import weakref
109
import logging
1110
import ctypes.util
@@ -78,78 +77,6 @@ def proxy(*args, **kwargs):
7877
return proxy
7978

8079

81-
# %% Enum
82-
83-
# We implement a custom enum class that's much simpler than Python's enum.Enum,
84-
# and simply maps to strings or ints. The enums are classes, so IDE's provide
85-
# autocompletion, and documenting with Sphinx is easy. That does mean we need a
86-
# metaclass though.
87-
88-
89-
class EnumType(type):
90-
"""Metaclass for enums and flags."""
91-
92-
def __new__(cls, name, bases, dct):
93-
# Collect and check fields
94-
member_map = {}
95-
for key, val in dct.items():
96-
if not key.startswith("_"):
97-
val = key if val is None else val
98-
if not isinstance(val, (int, str)):
99-
raise TypeError("Enum fields must be str or int.")
100-
member_map[key] = val
101-
# Some field values may have been updated
102-
dct.update(member_map)
103-
# Create class
104-
klass = super().__new__(cls, name, bases, dct)
105-
# Attach some fields
106-
klass.__fields__ = tuple(member_map)
107-
klass.__members__ = types.MappingProxyType(member_map) # enums.Enum compat
108-
# Create bound methods
109-
for name in ["__dir__", "__iter__", "__getitem__", "__setattr__", "__repr__"]:
110-
setattr(klass, name, types.MethodType(getattr(cls, name), klass))
111-
return klass
112-
113-
def __dir__(cls):
114-
# Support dir(enum). Note that this order matches the definition, but dir() makes it alphabetic.
115-
return cls.__fields__
116-
117-
def __iter__(cls):
118-
# Support list(enum), iterating over the enum, and doing ``x in enum``.
119-
return iter([getattr(cls, key) for key in cls.__fields__])
120-
121-
def __getitem__(cls, key):
122-
# Support enum[key]
123-
return cls.__dict__[key]
124-
125-
def __repr__(cls):
126-
if cls is BaseEnum:
127-
return "<rendercanvas.BaseEnum>"
128-
pkg = cls.__module__.split(".")[0]
129-
name = cls.__name__
130-
options = []
131-
for key in cls.__fields__:
132-
val = cls[key]
133-
options.append(f"'{key}' ({val})" if isinstance(val, int) else f"'{val}'")
134-
return f"<{pkg}.{name} enum with options: {', '.join(options)}>"
135-
136-
def __setattr__(cls, name, value):
137-
if name.startswith("_"):
138-
super().__setattr__(name, value)
139-
else:
140-
raise RuntimeError("Cannot set values on an enum.")
141-
142-
143-
class BaseEnum(metaclass=EnumType):
144-
"""Base class for flags and enums.
145-
146-
Looks like Python's builtin Enum class, but is simpler; fields are simply ints or strings.
147-
"""
148-
149-
def __init__(self):
150-
raise RuntimeError("Cannot instantiate an enum.")
151-
152-
15380
# %% lib support
15481

15582

rendercanvas/_enums.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import types
2+
from typing import Literal
3+
4+
# ----- Base enum class
5+
6+
# We implement a custom enum class that's much simpler than Python's enum.Enum,
7+
# and simply maps to strings or ints. The enums are classes, so IDE's provide
8+
# autocompletion, and documenting with Sphinx is easy. That does mean we need a
9+
# metaclass though.
10+
11+
12+
class EnumType(type):
13+
"""Metaclass for enums and flags."""
14+
15+
def __new__(cls, name, bases, dct):
16+
# Collect and check fields
17+
member_map = {}
18+
for key, val in dct.items():
19+
if not key.startswith("_"):
20+
val = key if val is None else val
21+
if not isinstance(val, (int, str)):
22+
raise TypeError("Enum fields must be str or int.")
23+
member_map[key] = val
24+
# Some field values may have been updated
25+
dct.update(member_map)
26+
# Create class
27+
klass = super().__new__(cls, name, bases, dct)
28+
# Attach some fields
29+
klass.__fields__ = tuple(member_map)
30+
klass.__members__ = types.MappingProxyType(member_map) # enums.Enum compat
31+
# Create bound methods
32+
for name in ["__dir__", "__iter__", "__getitem__", "__setattr__", "__repr__"]:
33+
setattr(klass, name, types.MethodType(getattr(cls, name), klass))
34+
return klass
35+
36+
def __dir__(cls):
37+
# Support dir(enum). Note that this order matches the definition, but dir() makes it alphabetic.
38+
return cls.__fields__
39+
40+
def __iter__(cls):
41+
# Support list(enum), iterating over the enum, and doing ``x in enum``.
42+
return iter([getattr(cls, key) for key in cls.__fields__])
43+
44+
def __getitem__(cls, key):
45+
# Support enum[key]
46+
return cls.__dict__[key]
47+
48+
def __repr__(cls):
49+
if cls is BaseEnum:
50+
return "<rendercanvas.BaseEnum>"
51+
pkg = cls.__module__.split(".")[0]
52+
name = cls.__name__
53+
options = []
54+
for key in cls.__fields__:
55+
val = cls[key]
56+
options.append(f"'{key}' ({val})" if isinstance(val, int) else f"'{val}'")
57+
return f"<{pkg}.{name} enum with options: {', '.join(options)}>"
58+
59+
def __setattr__(cls, name, value):
60+
if name.startswith("_"):
61+
super().__setattr__(name, value)
62+
else:
63+
raise RuntimeError("Cannot set values on an enum.")
64+
65+
66+
class BaseEnum(metaclass=EnumType):
67+
"""Base class for flags and enums.
68+
69+
Looks like Python's builtin Enum class, but is simpler; fields are simply ints or strings.
70+
"""
71+
72+
def __init__(self):
73+
raise RuntimeError("Cannot instantiate an enum.")
74+
75+
76+
# ----- The enums
77+
78+
# The Xxxx(BaseEnum) classes are for Sphynx docs, and maybe discovery in interactive sessions.
79+
# The XxxxEnum Literals are for type checking, and static autocompletion of string args in funcs that accept an enum.
80+
81+
82+
CursorShapeEnum = Literal[
83+
"default",
84+
"text",
85+
"crosshair",
86+
"pointer",
87+
"ew_resize",
88+
"ns_resize",
89+
"nesw_resize",
90+
"nwse_resize",
91+
"not_allowed",
92+
"none",
93+
]
94+
95+
96+
class CursorShape(BaseEnum):
97+
"""The CursorShape enum specifies the suppported cursor shapes, following CSS cursor names."""
98+
99+
default = None #: The platform-dependent default cursor, typically an arrow.
100+
text = None #: The text input I-beam cursor shape.
101+
crosshair = None #:
102+
pointer = None #: The pointing hand cursor shape.
103+
ew_resize = "ew-resize" #: The horizontal resize/move arrow shape.
104+
ns_resize = "ns-resize" #: The vertical resize/move arrow shape.
105+
nesw_resize = (
106+
"nesw-resize" #: The top-left to bottom-right diagonal resize/move arrow shape.
107+
)
108+
nwse_resize = (
109+
"nwse-resize" #: The top-right to bottom-left diagonal resize/move arrow shape.
110+
)
111+
not_allowed = "not-allowed" #: The operation-not-allowed shape.
112+
none = "none" #: The cursor is hidden.
113+
114+
115+
EventTypeEnum = Literal[
116+
"*",
117+
"resize",
118+
"close",
119+
"pointer_down",
120+
"pointer_up",
121+
"pointer_move",
122+
"pointer_enter",
123+
"pointer_leave",
124+
"double_click",
125+
"wheel",
126+
"key_down",
127+
"key_up",
128+
"char",
129+
"before_draw",
130+
"animate",
131+
]
132+
133+
134+
class EventType(BaseEnum):
135+
"""The EventType enum specifies the possible events for a RenderCanvas.
136+
137+
This includes the events from the jupyter_rfb event spec (see
138+
https://jupyter-rfb.readthedocs.io/en/stable/events.html) plus some
139+
rendercanvas-specific events.
140+
"""
141+
142+
# Jupter_rfb spec
143+
144+
resize = None #: The canvas has changed size. Has 'width' and 'height' in logical pixels, 'pixel_ratio'.
145+
close = None #: The canvas is closed. No additional fields.
146+
pointer_down = None #: The pointing device is pressed down. Has 'x', 'y', 'button', 'butons', 'modifiers', 'ntouches', 'touches'.
147+
pointer_up = None #: The pointing device is released. Same fields as pointer_down. Can occur outside of the canvas.
148+
pointer_move = None #: The pointing device is moved. Same fields as pointer_down. Can occur outside of the canvas if the pointer is currently down.
149+
pointer_enter = None #: The pointing device is moved into the canvas.
150+
pointer_leave = None #: The pointing device is moved outside of the canvas (regardless of a button currently being pressed).
151+
double_click = None #: A double-click / long-tap. This event looks like a pointer event, but without the touches.
152+
wheel = None #: The mouse-wheel is used (scrolling), or the touchpad/touchscreen is scrolled/pinched. Has 'dx', 'dy', 'x', 'y', 'modifiers'.
153+
key_down = None #: A key is pressed down. Has 'key', 'modifiers'.
154+
key_up = None #: A key is released. Has 'key', 'modifiers'.
155+
156+
# Pending for the spec, may become part of key_down/key_up
157+
char = None #: Experimental
158+
159+
# Our extra events
160+
161+
before_draw = (
162+
None #: Event emitted right before a draw is performed. Has no extra fields.
163+
)
164+
animate = None #: Animation event. Has 'step' representing the step size in seconds. This is stable, except when the 'catch_up' field is nonzero.
165+
166+
167+
UpdateModeEnum = Literal["manual", "ondemand", "continuous", "fastest"]
168+
169+
170+
class UpdateMode(BaseEnum):
171+
"""The UpdateMode enum specifies the different modes to schedule draws for the canvas."""
172+
173+
manual = None #: Draw events are never scheduled. Draws only happen when you ``canvas.force_draw()``, and maybe when the GUI system issues them (e.g. when resizing).
174+
ondemand = None #: Draws are only scheduled when ``canvas.request_draw()`` is called when an update is needed. Safes your laptop battery. Honours ``min_fps`` and ``max_fps``.
175+
continuous = None #: Continuously schedules draw events, honouring ``max_fps``. Calls to ``canvas.request_draw()`` have no effect.
176+
fastest = None #: Continuously schedules draw events as fast as possible. Gives high FPS (and drains your battery).

rendercanvas/_events.py

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,8 @@
66
from inspect import iscoroutinefunction
77
from collections import defaultdict, deque
88

9-
from ._coreutils import log_exception, BaseEnum
10-
11-
12-
class EventType(BaseEnum):
13-
"""The EventType enum specifies the possible events for a RenderCanvas.
14-
15-
This includes the events from the jupyter_rfb event spec (see
16-
https://jupyter-rfb.readthedocs.io/en/stable/events.html) plus some
17-
rendercanvas-specific events.
18-
"""
19-
20-
# Jupter_rfb spec
21-
22-
resize = None #: The canvas has changed size. Has 'width' and 'height' in logical pixels, 'pixel_ratio'.
23-
close = None #: The canvas is closed. No additional fields.
24-
pointer_down = None #: The pointing device is pressed down. Has 'x', 'y', 'button', 'butons', 'modifiers', 'ntouches', 'touches'.
25-
pointer_up = None #: The pointing device is released. Same fields as pointer_down. Can occur outside of the canvas.
26-
pointer_move = None #: The pointing device is moved. Same fields as pointer_down. Can occur outside of the canvas if the pointer is currently down.
27-
pointer_enter = None #: The pointing device is moved into the canvas.
28-
pointer_leave = None #: The pointing device is moved outside of the canvas (regardless of a button currently being pressed).
29-
double_click = None #: A double-click / long-tap. This event looks like a pointer event, but without the touches.
30-
wheel = None #: The mouse-wheel is used (scrolling), or the touchpad/touchscreen is scrolled/pinched. Has 'dx', 'dy', 'x', 'y', 'modifiers'.
31-
key_down = None #: A key is pressed down. Has 'key', 'modifiers'.
32-
key_up = None #: A key is released. Has 'key', 'modifiers'.
33-
34-
# Pending for the spec, may become part of key_down/key_up
35-
char = None #: Experimental
36-
37-
# Our extra events
38-
39-
before_draw = (
40-
None #: Event emitted right before a draw is performed. Has no extra fields.
41-
)
42-
animate = None #: Animation event. Has 'step' representing the step size in seconds. This is stable, except when the 'catch_up' field is nonzero.
9+
from ._coreutils import log_exception
10+
from ._enums import EventType
4311

4412

4513
valid_event_types = set(EventType)
@@ -80,10 +48,10 @@ def add_handler(self, *args, order: float = 0):
8048
"""Register an event handler to receive events.
8149
8250
Arguments:
83-
callback (callable): The event handler. Must accept a single event argument.
51+
callback (callable, optional): The event handler. Must accept a single event argument.
8452
Can be a plain function or a coroutine function.
8553
If you use async callbacks, see :ref:`async` for the limitations.
86-
*types (list of strings): A list of event types.
54+
*types (list of EventType): A list of event types.
8755
order (float): Set callback priority order. Callbacks with lower priorities
8856
are called first. Default is 0. When an event is emitted, callbacks with
8957
the same priority are called in the order that they were added.

rendercanvas/_scheduler.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,13 @@
66
import time
77
import weakref
88

9-
from ._coreutils import BaseEnum
9+
from ._enums import UpdateMode
1010
from .utils.asyncs import sleep, Event
1111

1212

1313
IS_WIN = sys.platform.startswith("win")
1414

1515

16-
class UpdateMode(BaseEnum):
17-
"""The UpdateMode enum specifies the different modes to schedule draws for the canvas."""
18-
19-
manual = None #: Draw events are never scheduled. Draws only happen when you ``canvas.force_draw()``, and maybe when the GUI system issues them (e.g. when resizing).
20-
ondemand = None #: Draws are only scheduled when ``canvas.request_draw()`` is called when an update is needed. Safes your laptop battery. Honours ``min_fps`` and ``max_fps``.
21-
continuous = None #: Continuously schedules draw events, honouring ``max_fps``. Calls to ``canvas.request_draw()`` have no effect.
22-
fastest = None #: Continuously schedules draw events as fast as possible. Gives high FPS (and drains your battery).
23-
24-
2516
class Scheduler:
2617
"""Helper class to schedule event processing and drawing."""
2718

0 commit comments

Comments
 (0)