Skip to content

Commit 61e4313

Browse files
authored
Add type hints for the public API (#85)
1 parent d8fd360 commit 61e4313

File tree

3 files changed

+89
-53
lines changed

3 files changed

+89
-53
lines changed

rendercanvas/_loop.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
The base loop implementation.
33
"""
44

5+
from __future__ import annotations
6+
57
import signal
68
from inspect import iscoroutinefunction
9+
from typing import TYPE_CHECKING
710

811
from ._coreutils import logger, log_exception
912
from .utils.asyncs import sleep
1013
from .utils import asyncadapter
1114

15+
if TYPE_CHECKING:
16+
from typing import Any, Callable, Coroutine, List
17+
from base import BaseRenderCanvas
18+
19+
CallbackFunction = Callable[[], Any]
20+
1221

1322
HANDLED_SIGNALS = (
1423
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@@ -74,7 +83,7 @@ def _unregister_canvas_group(self, canvas_group):
7483
# A CanvasGroup will call this when it selects a different loop.
7584
self.__canvas_groups.discard(canvas_group)
7685

77-
def get_canvases(self):
86+
def get_canvases(self) -> List[BaseRenderCanvas]:
7887
"""Get a list of currently active (not-closed) canvases."""
7988
canvases = []
8089
for canvas_group in self.__canvas_groups:
@@ -139,7 +148,12 @@ async def _loop_task(self):
139148
finally:
140149
self.__stop()
141150

142-
def add_task(self, async_func, *args, name="unnamed"):
151+
def add_task(
152+
self,
153+
async_func: Callable[[], Coroutine],
154+
*args: Any,
155+
name: str = "unnamed",
156+
) -> None:
143157
"""Run an async function in the event-loop.
144158
145159
All tasks are stopped when the loop stops.
@@ -154,7 +168,7 @@ async def wrapper():
154168

155169
self._rc_add_task(wrapper, name)
156170

157-
def call_soon(self, callback, *args):
171+
def call_soon(self, callback: CallbackFunction, *args: Any) -> None:
158172
"""Arrange for a callback to be called as soon as possible.
159173
160174
The callback will be called in the next iteration of the event-loop,
@@ -171,7 +185,7 @@ async def wrapper():
171185

172186
self._rc_add_task(wrapper, "call_soon")
173187

174-
def call_later(self, delay, callback, *args):
188+
def call_later(self, delay: float, callback: CallbackFunction, *args: Any) -> None:
175189
"""Arrange for a callback to be called after the given delay (in seconds)."""
176190
if delay <= 0:
177191
return self.call_soon(callback, *args)
@@ -188,7 +202,7 @@ async def wrapper():
188202

189203
self._rc_add_task(wrapper, "call_later")
190204

191-
def run(self):
205+
def run(self) -> None:
192206
"""Enter the main loop.
193207
194208
This provides a generic API to start the loop. When building an application (e.g. with Qt)
@@ -229,7 +243,7 @@ def run(self):
229243
for sig, cb in prev_sig_handlers.items():
230244
signal.signal(sig, cb)
231245

232-
async def run_async(self):
246+
async def run_async(self) -> None:
233247
""" "Alternative to ``run()``, to enter the mainloop from a running async framework.
234248
235249
Only supported by the asyncio and trio loops.
@@ -250,7 +264,7 @@ async def run_async(self):
250264

251265
await self._rc_run_async()
252266

253-
def stop(self):
267+
def stop(self) -> None:
254268
"""Close all windows and stop the currently running event-loop.
255269
256270
If the loop is active but not running via our ``run()`` method, the loop

rendercanvas/auto.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import os
88
import sys
99
import importlib
10+
from typing import cast
11+
1012
from ._coreutils import logger, QT_MODULE_NAMES, get_imported_qt_lib, asyncio_is_running
13+
from .base import BaseRenderCanvas, BaseLoop
1114

1215

1316
# Note that wx is not in here, because it does not (yet) fully implement base.BaseRenderCanvas
@@ -201,5 +204,5 @@ def backends_by_trying_in_order():
201204

202205
# Load!
203206
module = select_backend()
204-
RenderCanvas = module.RenderCanvas
205-
loop = module.loop
207+
RenderCanvas = cast(type[BaseRenderCanvas], module.RenderCanvas)
208+
loop = cast(BaseLoop, module.loop)

rendercanvas/base.py

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@
22
The base classes.
33
"""
44

5-
__all__ = ["BaseLoop", "BaseRenderCanvas", "WrapperRenderCanvas"]
5+
from __future__ import annotations
66

77
import sys
88
import weakref
99
import importlib
10-
from typing import Optional, Tuple
10+
from typing import TYPE_CHECKING
1111

1212
from ._events import EventEmitter, EventType # noqa: F401
1313
from ._loop import BaseLoop
1414
from ._scheduler import Scheduler, UpdateMode
1515
from ._coreutils import logger, log_exception, BaseEnum
1616

17+
if TYPE_CHECKING:
18+
from typing import Callable, List, Optional, Tuple
19+
20+
EventHandlerFunction = Callable[[dict], None]
21+
DrawFunction = Callable[[], None]
22+
23+
24+
__all__ = ["BaseLoop", "BaseRenderCanvas", "WrapperRenderCanvas"]
25+
1726

1827
# Notes on naming and prefixes:
1928
#
@@ -61,7 +70,7 @@ def _register_canvas(self, canvas, task):
6170
loop._register_canvas_group(self)
6271
loop.add_task(task, name="scheduler-task")
6372

64-
def select_loop(self, loop):
73+
def select_loop(self, loop: BaseLoop) -> None:
6574
"""Select the loop to use for this group of canvases."""
6675
if not (loop is None or isinstance(loop, BaseLoop)):
6776
raise TypeError("select_loop() requires a loop instance or None.")
@@ -74,11 +83,11 @@ def select_loop(self, loop):
7483
self._loop._unregister_canvas_group(self)
7584
self._loop = loop
7685

77-
def get_loop(self):
86+
def get_loop(self) -> BaseLoop:
7887
"""Get the currently associated loop (can be None for canvases that don't run a scheduler)."""
7988
return self._loop
8089

81-
def get_canvases(self):
90+
def get_canvases(self) -> List[BaseRenderCanvas]:
8291
"""Get a list of currently active (not-closed) canvases for this group."""
8392
return [canvas for canvas in self._canvases if not canvas.get_closed()]
8493

@@ -112,7 +121,7 @@ class BaseRenderCanvas:
112121
"""
113122

114123
@classmethod
115-
def select_loop(cls, loop):
124+
def select_loop(cls, loop: BaseLoop) -> None:
116125
"""Select the loop to run newly created canvases with.
117126
Can only be called when there are no live canvases of this class.
118127
"""
@@ -213,11 +222,11 @@ def __del__(self):
213222

214223
_canvas_context = None # set in get_context()
215224

216-
def get_physical_size(self):
225+
def get_physical_size(self) -> Tuple[int]:
217226
"""Get the physical size of the canvas in integer pixels."""
218227
return self._rc_get_physical_size()
219228

220-
def get_context(self, context_type):
229+
def get_context(self, context_type: str) -> object:
221230
"""Get a context object that can be used to render to this canvas.
222231
223232
The context takes care of presenting the rendered result to the canvas.
@@ -292,13 +301,15 @@ def get_context(self, context_type):
292301

293302
# %% Events
294303

295-
def add_event_handler(self, *args, **kwargs):
296-
return self._events.add_handler(*args, **kwargs)
304+
def add_event_handler(
305+
self, *args: str | EventHandlerFunction, order: float = 0
306+
) -> None:
307+
return self._events.add_handler(*args, order=order)
297308

298-
def remove_event_handler(self, *args, **kwargs):
299-
return self._events.remove_handler(*args, **kwargs)
309+
def remove_event_handler(self, callback: EventHandlerFunction, *types: str) -> None:
310+
return self._events.remove_handler(callback, *types)
300311

301-
def submit_event(self, event):
312+
def submit_event(self, event: dict) -> None:
302313
# Not strictly necessary for normal use-cases, but this allows
303314
# the ._event to be an implementation detail to subclasses, and it
304315
# allows users to e.g. emulate events in tests.
@@ -354,7 +365,13 @@ def _draw_frame(self):
354365
"""
355366
pass
356367

357-
def set_update_mode(self, update_mode, *, min_fps=None, max_fps=None):
368+
def set_update_mode(
369+
self,
370+
update_mode: UpdateMode,
371+
*,
372+
min_fps: Optional[float] = None,
373+
max_fps: Optional[float] = None,
374+
) -> None:
358375
"""Set the update mode for scheduling draws.
359376
360377
Arguments:
@@ -365,7 +382,7 @@ def set_update_mode(self, update_mode, *, min_fps=None, max_fps=None):
365382
"""
366383
self.__scheduler.set_update_mode(update_mode, min_fps=min_fps, max_fps=max_fps)
367384

368-
def request_draw(self, draw_function=None):
385+
def request_draw(self, draw_function: Optional[DrawFunction] = None) -> None:
369386
"""Schedule a new draw event.
370387
371388
This function does not perform a draw directly, but schedules a draw at
@@ -388,7 +405,7 @@ def request_draw(self, draw_function=None):
388405
# storing it here, the gc can detect this case, and its fine. However,
389406
# this fails if we'd store _draw_frame on the scheduler!
390407

391-
def force_draw(self):
408+
def force_draw(self) -> None:
392409
"""Perform a draw right now.
393410
394411
In most cases you want to use ``request_draw()``. If you find yourself using
@@ -458,15 +475,15 @@ def _draw_frame_and_present(self):
458475

459476
# %% Primary canvas management methods
460477

461-
def get_logical_size(self):
478+
def get_logical_size(self) -> Tuple[float]:
462479
"""Get the logical size (width, height) in float pixels."""
463480
return self._rc_get_logical_size()
464481

465-
def get_pixel_ratio(self):
482+
def get_pixel_ratio(self) -> float:
466483
"""Get the float ratio between logical and physical pixels."""
467484
return self._rc_get_pixel_ratio()
468485

469-
def close(self):
486+
def close(self) -> None:
470487
"""Close the canvas."""
471488
# Clear the draw-function, to avoid it holding onto e.g. wgpu objects.
472489
self._draw_frame = None
@@ -483,7 +500,7 @@ def close(self):
483500
# Let the subclass clean up.
484501
self._rc_close()
485502

486-
def get_closed(self):
503+
def get_closed(self) -> bool:
487504
"""Get whether the window is closed."""
488505
return self._rc_get_closed()
489506

@@ -498,14 +515,14 @@ def is_closed(self):
498515
# These methods provide extra control over the canvas. Subclasses should
499516
# implement the methods they can, but these features are likely not critical.
500517

501-
def set_logical_size(self, width, height):
518+
def set_logical_size(self, width: float, height: float) -> None:
502519
"""Set the window size (in logical pixels)."""
503520
width, height = float(width), float(height)
504521
if width < 0 or height < 0:
505522
raise ValueError("Canvas width and height must not be negative")
506523
self._rc_set_logical_size(width, height)
507524

508-
def set_title(self, title):
525+
def set_title(self, title: str) -> None:
509526
"""Set the window title.
510527
511528
The words "$backend", "$loop", and "$fps" can be used as variables that
@@ -516,7 +533,7 @@ def set_title(self, title):
516533
title = title.replace("$" + k, v)
517534
self._rc_set_title(title)
518535

519-
def set_cursor(self, cursor):
536+
def set_cursor(self, cursor: CursorShape) -> None:
520537
"""Set the cursor shape for the mouse pointer.
521538
522539
See :obj:`rendercanvas.CursorShape`:
@@ -658,50 +675,52 @@ class WrapperRenderCanvas(BaseRenderCanvas):
658675
_rc_canvas_group = None # No grouping for these wrappers
659676

660677
@classmethod
661-
def select_loop(cls, loop):
678+
def select_loop(cls, loop: BaseLoop) -> None:
662679
m = sys.modules[cls.__module__]
663680
return m.RenderWidget.select_loop(loop)
664681

665-
def add_event_handler(self, *args, **kwargs):
666-
return self._subwidget._events.add_handler(*args, **kwargs)
682+
def add_event_handler(
683+
self, *args: str | EventHandlerFunction, order: float = 0
684+
) -> None:
685+
return self._subwidget._events.add_handler(*args, order=order)
667686

668-
def remove_event_handler(self, *args, **kwargs):
669-
return self._subwidget._events.remove_handler(*args, **kwargs)
687+
def remove_event_handler(self, callback: EventHandlerFunction, *types: str) -> None:
688+
return self._subwidget._events.remove_handler(callback, *types)
670689

671-
def submit_event(self, event):
690+
def submit_event(self, event: dict) -> None:
672691
return self._subwidget._events.submit(event)
673692

674-
def get_context(self, *args, **kwargs):
675-
return self._subwidget.get_context(*args, **kwargs)
693+
def get_context(self, context_type: str) -> object:
694+
return self._subwidget.get_context(context_type)
676695

677-
def request_draw(self, *args, **kwargs):
678-
return self._subwidget.request_draw(*args, **kwargs)
696+
def request_draw(self, draw_function: Optional[DrawFunction] = None) -> None:
697+
return self._subwidget.request_draw(draw_function)
679698

680-
def force_draw(self):
699+
def force_draw(self) -> None:
681700
self._subwidget.force_draw()
682701

683-
def get_physical_size(self):
702+
def get_physical_size(self) -> Tuple[int]:
684703
return self._subwidget.get_physical_size()
685704

686-
def get_logical_size(self):
705+
def get_logical_size(self) -> Tuple[float]:
687706
return self._subwidget.get_logical_size()
688707

689-
def get_pixel_ratio(self):
708+
def get_pixel_ratio(self) -> float:
690709
return self._subwidget.get_pixel_ratio()
691710

692-
def set_logical_size(self, width, height):
711+
def set_logical_size(self, width: float, height: float) -> None:
693712
self._subwidget.set_logical_size(width, height)
694713

695-
def set_title(self, *args):
696-
self._subwidget.set_title(*args)
714+
def set_title(self, title: str) -> None:
715+
self._subwidget.set_title(title)
697716

698-
def set_cursor(self, *args):
699-
self._subwidget.set_cursor(*args)
717+
def set_cursor(self, cursor: CursorShape) -> None:
718+
self._subwidget.set_cursor(cursor)
700719

701-
def close(self):
720+
def close(self) -> None:
702721
self._subwidget.close()
703722

704-
def get_closed(self):
723+
def get_closed(self) -> bool:
705724
return self._subwidget.get_closed()
706725

707726
def is_closed(self):

0 commit comments

Comments
 (0)