rendercanvas 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/PKG-INFO +3 -4
  2. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/README.md +1 -2
  3. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/pyproject.toml +1 -1
  4. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/__init__.py +2 -3
  5. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/_coreutils.py +23 -80
  6. rendercanvas-2.3.0/rendercanvas/_enums.py +176 -0
  7. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/_events.py +23 -53
  8. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/_loop.py +9 -4
  9. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/_scheduler.py +46 -40
  10. rendercanvas-2.3.0/rendercanvas/_version.py +194 -0
  11. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/auto.py +3 -1
  12. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/base.py +139 -77
  13. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/glfw.py +16 -43
  14. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/jupyter.py +11 -18
  15. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/offscreen.py +26 -13
  16. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/qt.py +20 -36
  17. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/raw.py +1 -0
  18. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/stub.py +5 -11
  19. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/bitmappresentadapter.py +1 -1
  20. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/cube.py +6 -3
  21. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/wx.py +9 -26
  22. rendercanvas-2.2.0/rendercanvas/_version.py +0 -113
  23. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/LICENSE +0 -0
  24. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/__pyinstaller/__init__.py +0 -0
  25. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/__pyinstaller/conftest.py +0 -0
  26. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/__pyinstaller/hook-rendercanvas.py +0 -0
  27. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/__pyinstaller/test_rendercanvas.py +0 -0
  28. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/_context.py +0 -0
  29. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/asyncio.py +0 -0
  30. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/pyqt5.py +0 -0
  31. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/pyqt6.py +0 -0
  32. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/pyside2.py +0 -0
  33. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/pyside6.py +0 -0
  34. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/trio.py +0 -0
  35. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/__init__.py +0 -0
  36. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/asyncadapter.py +0 -0
  37. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/asyncs.py +0 -0
  38. {rendercanvas-2.2.0 → rendercanvas-2.3.0}/rendercanvas/utils/bitmaprenderingcontext.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rendercanvas
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: One canvas API, multiple backends
5
5
  Keywords: canvas,rendering,graphics,wgpu,qt,wx,glfw,jupyter
6
6
  Author: Almar Klein, Korijn van Golen
7
- Requires-Python: >= 3.9
7
+ Requires-Python: >= 3.10
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: sniffio
@@ -45,8 +45,7 @@ Provides-Extra: tests
45
45
  [![PyPI version](https://badge.fury.io/py/rendercanvas.svg)](https://badge.fury.io/py/rendercanvas)
46
46
  [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver)
47
47
 
48
-
49
- # rendercanvas
48
+ <h1 align="center"><img src="https://github.com/user-attachments/assets/74ddfc1e-03ae-4965-afe9-8697ec4614b5" height="128"><br>rendercanvas</h1>
50
49
 
51
50
  One canvas API, multiple backends 🚀
52
51
 
@@ -3,8 +3,7 @@
3
3
  [![PyPI version](https://badge.fury.io/py/rendercanvas.svg)](https://badge.fury.io/py/rendercanvas)
4
4
  [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver)
5
5
 
6
-
7
- # rendercanvas
6
+ <h1 align="center"><img src="https://github.com/user-attachments/assets/74ddfc1e-03ae-4965-afe9-8697ec4614b5" height="128"><br>rendercanvas</h1>
8
7
 
9
8
  One canvas API, multiple backends 🚀
10
9
 
@@ -17,7 +17,7 @@ keywords = [
17
17
  "glfw",
18
18
  "jupyter",
19
19
  ]
20
- requires-python = ">= 3.9"
20
+ requires-python = ">= 3.10"
21
21
  dependencies = ["sniffio"]
22
22
  [project.optional-dependencies]
23
23
  # For users
@@ -6,8 +6,7 @@ RenderCanvas: one canvas API, multiple backends.
6
6
 
7
7
  from ._version import __version__, version_info
8
8
  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
12
11
 
13
12
  __all__ = ["BaseLoop", "BaseRenderCanvas", "CursorShape", "EventType", "UpdateMode"]
@@ -5,7 +5,7 @@ Core utilities that are loaded into the root namespace or used internally.
5
5
  import os
6
6
  import re
7
7
  import sys
8
- import types
8
+ import time
9
9
  import weakref
10
10
  import logging
11
11
  import ctypes.util
@@ -19,7 +19,7 @@ logger = logging.getLogger("rendercanvas")
19
19
  logger.setLevel(logging.WARNING)
20
20
 
21
21
 
22
- err_hashes = {}
22
+ err_hashes = {} # hash -> [short-message, count, next-time]
23
23
 
24
24
  _re_wgpu_ob = re.compile(r"`<[a-z|A-Z]+-\([0-9]+, [0-9]+, [a-z|A-Z]+\)>`")
25
25
 
@@ -42,22 +42,37 @@ def log_exception(kind):
42
42
  except Exception as err:
43
43
  # Store exc info for postmortem debugging
44
44
  exc_info = list(sys.exc_info())
45
- exc_info[2] = exc_info[2].tb_next # skip *this* function
45
+ exc_info[2] = exc_info[2].tb_next # type: ignore | skip *this* function
46
46
  sys.last_type, sys.last_value, sys.last_traceback = exc_info
47
47
  # Show traceback, or a one-line summary
48
48
  msg = str(err)
49
49
  msgh = error_message_hash(msg)
50
50
  if msgh not in err_hashes:
51
+ # Prepare a short variant of the message for later use
52
+ short_msg = kind + ": " + msg.split("\n")[0].strip()
53
+ short_msg = short_msg if len(short_msg) <= 70 else short_msg[:69] + "…"
54
+ err_hashes[msgh] = [short_msg, 1, 0]
51
55
  # Provide the exception, so the default logger prints a stacktrace.
52
56
  # IDE's can get the exception from the root logger for PM debugging.
53
- err_hashes[msgh] = 1
54
57
  logger.error(kind, exc_info=err)
55
58
  else:
56
59
  # We've seen this message before, return a one-liner instead.
57
- err_hashes[msgh] = count = err_hashes[msgh] + 1
58
- msg = kind + ": " + msg.split("\n")[0].strip()
59
- msg = msg if len(msg) <= 70 else msg[:69] + "…"
60
- logger.error(msg + f" ({count})")
60
+ short_count_tm = err_hashes[msgh]
61
+ short, count, tm = short_count_tm
62
+ short_count_tm[1] = count = count + 1
63
+ # Show the message now?
64
+ show_message = False
65
+ cur_time = time.perf_counter()
66
+ if count <= 5:
67
+ show_message = True
68
+ else:
69
+ if cur_time > tm:
70
+ show_message = True
71
+ # Log the messages and schedule when to show it next.
72
+ # Next message is after 1-3 seconds (3 when count reaches 300).
73
+ if show_message:
74
+ short_count_tm[2] = cur_time + min(max(count / 100, 1), 3)
75
+ logger.error(f"{short} ({count})")
61
76
 
62
77
 
63
78
  # %% Weak bindings
@@ -78,78 +93,6 @@ def weakbind(method):
78
93
  return proxy
79
94
 
80
95
 
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
-
153
96
  # %% lib support
154
97
 
155
98
 
@@ -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).
@@ -5,41 +5,10 @@ The event system.
5
5
  import time
6
6
  from inspect import iscoroutinefunction
7
7
  from collections import defaultdict, deque
8
+ from typing import Callable
8
9
 
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.
10
+ from ._coreutils import log_exception
11
+ from ._enums import EventType
43
12
 
44
13
 
45
14
  valid_event_types = set(EventType)
@@ -76,14 +45,12 @@ class EventEmitter:
76
45
  self._pending_events.clear()
77
46
  self._event_handlers.clear()
78
47
 
79
- def add_handler(self, *args, order: float = 0):
48
+ def add_handler(self, *args, order: float = 0) -> Callable:
80
49
  """Register an event handler to receive events.
81
50
 
82
51
  Arguments:
83
- callback (callable): The event handler. Must accept a single event argument.
84
- Can be a plain function or a coroutine function.
85
- If you use async callbacks, see :ref:`async` for the limitations.
86
- *types (list of strings): A list of event types.
52
+ callback (callable, optional): The event handler. Must accept a single event argument.
53
+ *types (list of EventType): A list of event types.
87
54
  order (float): Set callback priority order. Callbacks with lower priorities
88
55
  are called first. Default is 0. When an event is emitted, callbacks with
89
56
  the same priority are called in the order that they were added.
@@ -121,9 +88,11 @@ class EventEmitter:
121
88
 
122
89
  """
123
90
  order = float(order)
124
- decorating = not callable(args[0])
125
- callback = None if decorating else args[0]
126
- types = args if decorating else args[1:]
91
+ callback = None
92
+ types = args
93
+ if len(args) > 0 and callable(args[0]):
94
+ callback = args[0]
95
+ types = args[1:]
127
96
 
128
97
  if not types:
129
98
  raise TypeError("No event types are given to add_event_handler.")
@@ -133,15 +102,19 @@ class EventEmitter:
133
102
  if not (type == "*" or type in valid_event_types):
134
103
  raise ValueError(f"Adding handler with invalid event_type: '{type}'")
135
104
 
136
- def decorator(_callback):
105
+ def decorator(_callback: Callable):
137
106
  self._add_handler(_callback, order, *types)
138
107
  return _callback
139
108
 
140
- if decorating:
109
+ if callback is None:
141
110
  return decorator
142
111
  return decorator(callback)
143
112
 
144
113
  def _add_handler(self, callback, order, *types):
114
+ if iscoroutinefunction(callback):
115
+ raise TypeError(
116
+ "Coroutines cannot be callbacks (anymore). Instead, you can use a regular callback that calls `loop.add_task(coro)`."
117
+ )
145
118
  if self._closed:
146
119
  return
147
120
  self.remove_handler(callback, *types)
@@ -195,7 +168,7 @@ class EventEmitter:
195
168
 
196
169
  self._pending_events.append(event)
197
170
 
198
- async def flush(self):
171
+ def flush(self):
199
172
  """Dispatch all pending events.
200
173
 
201
174
  This should generally be left to the scheduler.
@@ -205,9 +178,9 @@ class EventEmitter:
205
178
  event = self._pending_events.popleft()
206
179
  except IndexError:
207
180
  break
208
- await self.emit(event)
181
+ self.emit(event)
209
182
 
210
- async def emit(self, event):
183
+ def emit(self, event):
211
184
  """Directly emit the given event.
212
185
 
213
186
  In most cases events should be submitted, so that they are flushed
@@ -221,15 +194,12 @@ class EventEmitter:
221
194
  if event.get("stop_propagation", False):
222
195
  break
223
196
  with log_exception(f"Error during handling {event_type} event"):
224
- if iscoroutinefunction(callback):
225
- await callback(event)
226
- else:
227
- callback(event)
197
+ callback(event)
228
198
  # Close?
229
199
  if event_type == "close":
230
200
  self._release()
231
201
 
232
- async def close(self):
202
+ def close(self):
233
203
  """Close the event handler.
234
204
 
235
205
  Drops all pending events, send the close event, and disables the emitter.
@@ -238,4 +208,4 @@ class EventEmitter:
238
208
  if not self._closed:
239
209
  self._pending_events.clear()
240
210
  self.submit({"event_type": "close"})
241
- await self.flush()
211
+ self.flush()
@@ -117,7 +117,7 @@ class BaseLoop:
117
117
  closed_canvas_ids = set(event_emitters) - set(new_event_emitters)
118
118
  for canvas_id in closed_canvas_ids:
119
119
  events = event_emitters[canvas_id]
120
- await events.close()
120
+ events.close()
121
121
 
122
122
  # Keep canvases alive
123
123
  for canvas in canvases:
@@ -139,9 +139,14 @@ class BaseLoop:
139
139
  break
140
140
  elif self.__should_stop:
141
141
  # Close all remaining canvases. Loop will stop in a next iteration.
142
+ # We store a flag on the canvas, that we only use here.
142
143
  for canvas in self.get_canvases():
143
- if not getattr(canvas, "_rc_closed_by_loop", False):
144
- canvas._rc_closed_by_loop = True
144
+ try:
145
+ closed_by_loop = canvas._rc_closed_by_loop # type: ignore
146
+ except AttributeError:
147
+ closed_by_loop = False
148
+ if not closed_by_loop:
149
+ canvas._rc_closed_by_loop = True # type: ignore
145
150
  canvas.close()
146
151
  del canvas
147
152
 
@@ -372,7 +377,7 @@ class BaseLoop:
372
377
  This method is optional. A subclass must either implement ``_rc_add_task`` or ``_rc_call_later``.
373
378
 
374
379
  * If you implememt this, make ``_rc_add_task()`` call ``super()._rc_add_task()``.
375
- * If delay is zero, this should behave like "call_later".
380
+ * If delay is zero, this should behave like "call_soon".
376
381
  * No need to catch errors from the callback; that's dealt with internally.
377
382
  * Return None.
378
383
  """