diff --git a/docs/backendapi.rst b/docs/backendapi.rst index 22d1672..c59ed73 100644 --- a/docs/backendapi.rst +++ b/docs/backendapi.rst @@ -1,3 +1,27 @@ -Backend API -=========== -TODO \ No newline at end of file +Internal backend API +==================== + +This page documents what's needed to implement a backend for ``rendercanvas``. The purpose of this documentation is +to help maintain current and new backends. Making this internal API clear helps understanding how the backend-system works. +Also see https://github.com/pygfx/rendercanvas/blob/main/rendercanvas/stub.py. + +It is possible to create a custom backend (outside of the ``rendercanvas`` package). However, we consider this API an internal detail that may change +with each version without warning. + +.. autoclass:: rendercanvas.base.WrapperRenderCanvas + +.. autoclass:: rendercanvas.stub.StubRenderCanvas + :members: + :private-members: + :member-order: bysource + + +.. autoclass:: rendercanvas.stub.StubTimer + :members: + :private-members: + :member-order: bysource + +.. autoclass:: rendercanvas.stub.StubLoop + :members: + :private-members: + :member-order: bysource diff --git a/docs/conf.py b/docs/conf.py index f751318..f4b5ead 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,7 @@ # Load wglibu so autodoc can query docstrings import rendercanvas # noqa: E402 +import rendercanvas.stub # noqa: E402 - we use the stub backend to generate doccs # -- Project information ----------------------------------------------------- diff --git a/rendercanvas/_events.py b/rendercanvas/_events.py index c51c744..a60a304 100644 --- a/rendercanvas/_events.py +++ b/rendercanvas/_events.py @@ -1,3 +1,7 @@ +""" +The event system. +""" + import time from collections import defaultdict, deque diff --git a/rendercanvas/_loop.py b/rendercanvas/_loop.py index 102ea3a..b820e81 100644 --- a/rendercanvas/_loop.py +++ b/rendercanvas/_loop.py @@ -1,5 +1,5 @@ """ -Implemens loop mechanics: The base timer, base loop, and scheduler. +The loop mechanics: the base timer, base loop, and scheduler. """ import time @@ -43,13 +43,13 @@ def start(self, interval): restarted. """ if self._interval is None: - self._init() + self._rc_init() if self.is_running: - self._stop() + self._rc_stop() BaseTimer._running_timers.add(self) self._interval = max(0.0, float(interval)) self._expect_tick_at = time.perf_counter() + self._interval - self._start() + self._rc_start() def stop(self): """Stop the timer. @@ -60,7 +60,7 @@ def stop(self): """ BaseTimer._running_timers.discard(self) self._expect_tick_at = None - self._stop() + self._rc_stop() def _tick(self): """The implementations must call this method.""" @@ -70,7 +70,7 @@ def _tick(self): self._expect_tick_at = None else: self._expect_tick_at = time.perf_counter() + self._interval - self._start() + self._rc_start() # Callback with log_exception("Timer callback error"): self._callback(*self._args) @@ -107,25 +107,25 @@ def is_one_shot(self): """ return self._one_shot - def _init(self): - """For the subclass to implement: + def _rc_init(self): + """Initialize the (native) timer object. Opportunity to initialize the timer object. This is called right before the timer is first started. """ pass - def _start(self): - """For the subclass to implement: + def _rc_start(self): + """Start the timer. * Must schedule for ``self._tick`` to be called in ``self._interval`` seconds. * Must call it exactly once (the base class takes care of repeating the timer). - * When ``self._stop()`` is called before the timer finished, the call to ``self._tick()`` must be cancelled. + * When ``self._rc_stop()`` is called before the timer finished, the call to ``self._tick()`` must be cancelled. """ raise NotImplementedError() - def _stop(self): - """For the subclass to implement: + def _rc_stop(self): + """Stop the timer. * If the timer is running, cancel the pending call to ``self._tick()``. * Otherwise, this should do nothing. @@ -183,7 +183,7 @@ def call_soon(self, callback, *args): The callback will be called in the next iteration of the event-loop, but other pending events/callbacks may be handled first. Returns None. """ - self._call_soon(callback, *args) + self._rc_call_soon(callback, *args) def call_later(self, delay, callback, *args): """Arrange for a callback to be called after the given delay (in seconds). @@ -220,14 +220,14 @@ def run(self, stop_when_no_canvases=True): its fine to start the loop in the normal way. """ self._stop_when_no_canvases = bool(stop_when_no_canvases) - self._run() + self._rc_run() def stop(self): """Stop the currently running event loop.""" - self._stop() + self._rc_stop() - def _run(self): - """For the subclass to implement: + def _rc_run(self): + """Start running the event-loop. * Start the event loop. * The rest of the loop object must work just fine, also when the loop is @@ -235,16 +235,16 @@ def _run(self): """ raise NotImplementedError() - def _stop(self): - """For the subclass to implement: + def _rc_stop(self): + """Stop the event loop. * Stop the running event loop. * When running in an interactive session, this call should probably be ignored. """ raise NotImplementedError() - def _call_soon(self, callback, *args): - """For the subclass to implement: + def _rc_call_soon(self, callback, *args): + """Method to call a callback in the next iteraction of the event-loop. * A quick path to have callback called in a next invocation of the event loop. * This method is optional: the default implementation just calls ``call_later()`` with a zero delay. @@ -252,7 +252,7 @@ def _call_soon(self, callback, *args): self.call_later(0, callback, *args) def _rc_gui_poll(self): - """For the subclass to implement: + """Process GUI events. Some event loops (e.g. asyncio) are just that and dont have a GUI to update. Other loops (like Qt) already process events. So this is only intended for diff --git a/rendercanvas/asyncio.py b/rendercanvas/asyncio.py index 161db1f..f3a2072 100644 --- a/rendercanvas/asyncio.py +++ b/rendercanvas/asyncio.py @@ -1,9 +1,13 @@ -"""Implements an asyncio event loop.""" +""" +Implements an asyncio event loop, used in some backends. +""" # This is used for backends that don't have an event loop by themselves, like glfw. # Would be nice to also allow a loop based on e.g. Trio. But we can likely fit that in # when the time comes. +__all__ = ["AsyncioLoop", "AsyncioTimer"] + import asyncio from .base import BaseLoop, BaseTimer @@ -14,7 +18,10 @@ class AsyncioTimer(BaseTimer): _handle = None - def _start(self): + def _rc_init(self): + pass + + def _rc_start(self): def tick(): self._handle = None self._tick() @@ -24,7 +31,7 @@ def tick(): asyncio_loop = self._loop._loop self._handle = asyncio_loop.call_later(self._interval, tick) - def _stop(self): + def _rc_stop(self): if self._handle: self._handle.cancel() self._handle = None @@ -56,16 +63,19 @@ def _get_loop(self): asyncio.set_event_loop(loop) return loop - def _run(self): + def _rc_run(self): if self._loop.is_running(): self._is_interactive = True else: self._is_interactive = False self._loop.run_forever() - def _stop(self): + def _rc_stop(self): if not self._is_interactive: self._loop.stop() - def _call_soon(self, callback, *args): + def _rc_call_soon(self, callback, *args): self._loop.call_soon(callback, *args) + + def _rc_gui_poll(self): + pass diff --git a/rendercanvas/auto.py b/rendercanvas/auto.py index aa9c8e0..02e414d 100644 --- a/rendercanvas/auto.py +++ b/rendercanvas/auto.py @@ -1,11 +1,8 @@ """ Automatic backend selection. - -Right now we only chose between GLFW, Qt and Jupyter. We might add support -for e.g. wx later. Or we might decide to stick with these three. """ -__all__ = ["RenderCanvas", "loop", "run"] +__all__ = ["RenderCanvas", "loop"] import os import sys @@ -188,5 +185,6 @@ def backends_by_trying_in_order(): # Load! module = select_backend() -RenderCanvas, loop = module.RenderCanvas, module.loop +RenderCanvas = module.RenderCanvas +loop = module.loop run = loop.run # backwards compat diff --git a/rendercanvas/base.py b/rendercanvas/base.py index 5ccf23b..1a99ae3 100644 --- a/rendercanvas/base.py +++ b/rendercanvas/base.py @@ -1,7 +1,13 @@ +""" +The base classes. +""" + +__all__ = ["WrapperRenderCanvas", "BaseRenderCanvas", "BaseLoop", "BaseTimer"] + import sys from ._events import EventEmitter, EventType # noqa: F401 -from ._loop import Scheduler, BaseLoop, BaseTimer # noqa: F401 +from ._loop import Scheduler, BaseLoop, BaseTimer from ._coreutils import log_exception @@ -213,7 +219,11 @@ def submit_event(self, event): # %% Scheduling and drawing def _process_events(self): - """Process events and animations. Called from the scheduler.""" + """Process events and animations. + + Called from the scheduler. + Subclasses *may* call this if the time between ``_rc_request_draw`` and the actual draw is relatively long. + """ # We don't want this to be called too often, because we want the # accumulative events to accumulate. Once per draw, and at max_fps @@ -393,7 +403,7 @@ def _rc_request_draw(self): """Request the GUI layer to perform a draw. Like requestAnimationFrame in JS. The draw must be performed - by calling _draw_frame_and_present(). It's the responsibility + by calling ``_draw_frame_and_present()``. It's the responsibility for the canvas subclass to make sure that a draw is made as soon as possible. @@ -411,7 +421,7 @@ def _rc_force_draw(self): """Perform a synchronous draw. When it returns, the draw must have been done. - The default implementation just calls _draw_frame_and_present(). + The default implementation just calls ``_draw_frame_and_present()``. """ self._draw_frame_and_present() @@ -441,7 +451,7 @@ def _rc_set_logical_size(self, width, height): def _rc_close(self): """Close the canvas. - For widgets that are wrapped by a WrapperRenderCanvas, this should probably + For widgets that are wrapped by a ``WrapperRenderCanvas``, this should probably close the wrapper instead. Note that ``BaseRenderCanvas`` implements the ``close()`` method, which @@ -456,7 +466,7 @@ def _rc_is_closed(self): def _rc_set_title(self, title): """Set the canvas title. May be ignored when it makes no sense. - For widgets that are wrapped by a WrapperRenderCanvas, this should probably + For widgets that are wrapped by a ``WrapperRenderCanvas``, this should probably set the title of the wrapper instead. The default implementation does nothing. @@ -468,7 +478,8 @@ class WrapperRenderCanvas(BaseRenderCanvas): """A base render canvas for top-level windows that wrap a widget, as used in e.g. Qt and wx. This base class implements all the re-direction logic, so that the subclass does not have to. - Wrapper classes should not implement any of the ``_rc_`` methods. + Subclasses should not implement any of the ``_rc_`` methods. Subclasses must instantiate the + wrapped canvas and set it as ``_subwidget``. """ # Events diff --git a/rendercanvas/glfw.py b/rendercanvas/glfw.py index 1a8b9c3..0e8525d 100644 --- a/rendercanvas/glfw.py +++ b/rendercanvas/glfw.py @@ -7,6 +7,8 @@ or ``sudo apt install libglfw3-wayland`` when using Wayland. """ +__all__ = ["RenderCanvas", "loop"] + import sys import time import atexit @@ -539,8 +541,8 @@ def _rc_gui_poll(self): if self.stop_if_no_more_canvases and not tuple(self.all_glfw_canvases): self.stop() - def _run(self): - super()._run() + def _rc_run(self): + super()._rc_run() if not self._is_interactive: poll_glfw_briefly() diff --git a/rendercanvas/jupyter.py b/rendercanvas/jupyter.py index 17774de..ad1d294 100644 --- a/rendercanvas/jupyter.py +++ b/rendercanvas/jupyter.py @@ -3,6 +3,8 @@ can be used as cell output, or embedded in an ipywidgets gui. """ +__all__ = ["RenderCanvas", "loop"] + import time import weakref @@ -142,3 +144,4 @@ def run(self): loop = JupyterAsyncioLoop() +run = loop.run diff --git a/rendercanvas/offscreen.py b/rendercanvas/offscreen.py index 9765f92..020905f 100644 --- a/rendercanvas/offscreen.py +++ b/rendercanvas/offscreen.py @@ -1,3 +1,9 @@ +""" +Offscreen canvas. No scheduling. +""" + +__all__ = ["RenderCanvas", "loop"] + from .base import BaseRenderCanvas, BaseLoop, BaseTimer @@ -44,7 +50,7 @@ def _rc_get_physical_size(self): def _rc_get_logical_size(self): return self._logical_size - def rc_get_pixel_ratio(self): + def _rc_get_pixel_ratio(self): return self._pixel_ratio def _rc_set_logical_size(self, width, height): @@ -79,10 +85,13 @@ def draw(self): class StubTimer(BaseTimer): - def _start(self): + def _rc_init(self): + pass + + def _rc_start(self): pass - def _stop(self): + def _rc_stop(self): pass @@ -108,11 +117,18 @@ def _process_timers(self): if timer.time_left <= 0: timer._tick() - def _run(self): + def _rc_run(self): self._process_timers() - def _stop(self): + def _rc_stop(self): + pass + + def _rc_call_soon(self, callback): + super()._rc_call_soon(callback) + + def _rc_gui_poll(self): pass loop = StubLoop() +run = loop.run diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index 06eb257..5c3ddf3 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -3,6 +3,8 @@ can be used as a standalone window or in a larger GUI. """ +__all__ = ["RenderCanvas", "RenderWidget", "QRenderWidget", "loop"] + import sys import ctypes import importlib @@ -514,16 +516,16 @@ def closeEvent(self, event): # noqa: N802 class QtTimer(BaseTimer): """Timer basef on Qt.""" - def _init(self): + def _rc_init(self): self._qt_timer = QtCore.QTimer() self._qt_timer.timeout.connect(self._tick) self._qt_timer.setSingleShot(True) self._qt_timer.setTimerType(PreciseTimer) - def _start(self): + def _rc_start(self): self._qt_timer.start(int(self._interval * 1000)) - def _stop(self): + def _rc_stop(self): self._qt_timer.stop() @@ -539,13 +541,13 @@ def _app(self): """Return global instance of Qt app instance or create one if not created yet.""" return QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - def _call_soon(self, callback, *args): + def _rc_call_soon(self, callback, *args): func = callback if args: func = lambda: callback(*args) QtCore.QTimer.singleshot(0, func) - def _run(self): + def _rc_run(self): # Note: we could detect if asyncio is running (interactive session) and wheter # we can use QtAsyncio. However, there's no point because that's up for the # end-user to decide. @@ -562,7 +564,7 @@ def _run(self): app.setQuitOnLastWindowClosed(False) app.exec() if hasattr(app, "exec") else app.exec_() - def _stop(self): + def _rc_stop(self): if not already_had_app_on_import: self._app.quit() diff --git a/rendercanvas/stub.py b/rendercanvas/stub.py new file mode 100644 index 0000000..d68993f --- /dev/null +++ b/rendercanvas/stub.py @@ -0,0 +1,120 @@ +""" +A stub backend for documentation purposes. +""" + +__all__ = ["RenderCanvas", "loop"] + +from .base import WrapperRenderCanvas, BaseRenderCanvas, BaseLoop, BaseTimer + + +class StubRenderCanvas(BaseRenderCanvas): + """ + Backends must subclass ``BaseRenderCanvas`` and implement a set of methods prefixed with ``_rc_``. + This class also shows a few other private methods of the base canvas class, that a backend must be aware of. + """ + + # Note that the methods below don't have docstrings, but Sphinx recovers the docstrings from the base class. + + # Just listed here so they end up in the docs + + def _final_canvas_init(self): + return super()._final_canvas_init() + + def _process_events(self): + return super()._process_events() + + def _draw_frame_and_present(self): + return super()._draw_frame_and_present() + + # Must be implemented by subclasses. + + def _rc_get_loop(self): + return None + + def _rc_get_present_info(self): + raise NotImplementedError() + + def _rc_request_draw(self): + pass + + def _rc_force_draw(self): + self._draw_frame_and_present() + + def _rc_present_image(self, image, **kwargs): + raise NotImplementedError() + + def _rc_get_physical_size(self): + raise NotImplementedError() + + def _rc_get_logical_size(self): + raise NotImplementedError() + + def _rc_get_pixel_ratio(self): + raise NotImplementedError() + + def _rc_set_logical_size(self, width, height): + pass + + def _rc_close(self): + pass + + def _rc_is_closed(self): + return False + + def _rc_set_title(self, title): + pass + + +class ToplevelRenderCanvas(WrapperRenderCanvas): + """ + Some backends require a toplevel wrapper. These can inherit from ``WrapperRenderCanvas``. + These have to instantiate the wrapped canvas and set it as ``_subwidget``. Implementations + are typically very small. + """ + + def __init__(self, parent=None, **kwargs): + super().__init__(parent) + + self._subwidget = StubRenderCanvas(self, **kwargs) + + +class StubTimer(BaseTimer): + """ + Backends must subclass ``BaseTimer`` and implement a set of methods prefixed with ``_rc__``. + """ + + def _rc_init(self): + pass + + def _rc_start(self): + raise NotImplementedError() + + def _rc_stop(self): + raise NotImplementedError() + + +class StubLoop(BaseLoop): + """ + Backends must subclass ``BaseLoop`` and implement a set of methods prefixed with ``_rc__``. + In addition to that, the class attribute ``_TimerClass`` must be set to the corresponding timer subclass. + """ + + _TimerClass = StubTimer + + def _rc_run(self): + raise NotImplementedError() + + def _rc_stop(self): + raise NotImplementedError() + + def _rc_call_soon(self, callback, *args): + self.call_later(0, callback, *args) + + def _rc_gui_poll(self): + pass + + +# Make available under a common name +RenderCanvas = StubRenderCanvas +loop = StubLoop() +run = loop.run diff --git a/rendercanvas/wx.py b/rendercanvas/wx.py index 415b704..4111fd7 100644 --- a/rendercanvas/wx.py +++ b/rendercanvas/wx.py @@ -3,6 +3,8 @@ can be used as a standalone window or in a larger GUI. """ +__all__ = ["RenderCanvas", "RenderWidget", "WxRenderWidget", "loop"] + import sys import time import ctypes @@ -467,13 +469,13 @@ def Notify(self, *args): # noqa: N802 class WxTimer(BaseTimer): - def _init(self): + def _rc_init(self): self._wx_timer = TimerWithCallback(self._tick) - def _start(self): + def _rc_start(self): self._wx_timer.StartOnce(int(self._interval * 1000)) - def _stop(self): + def _rc_stop(self): self._wx_timer.Stop() @@ -493,14 +495,14 @@ def _app(self): wx.App.SetInstance(app) return app - def _call_soon(self, delay, callback, *args): + def _rc_call_soon(self, delay, callback, *args): wx.CallSoon(callback, args) - def _run(self): + def _rc_run(self): self._frame_to_keep_loop_alive = wx.Frame(None) self._app.MainLoop() - def _stop(self): + def _rc_stop(self): self._frame_to_keep_loop_alive.Destroy() _frame_to_keep_loop_alive = None diff --git a/tests/test_backends.py b/tests/test_backends.py new file mode 100644 index 0000000..8247ac1 --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,321 @@ +""" +Test basic validity of the backends. + +* Test that each backend module has the expected names in its namespace. +* Test that the classes (canvas, loop, timer) implement the correct _rx_xx methods. +""" + +import os +import ast + +import rendercanvas +from testutils import run_tests + + +# %% Helper + + +def get_ref_rc_methods(what): + """Get the reference rc-methods from the respective Python objects.""" + cls = getattr(rendercanvas, "Base" + what) + rc_methods = set() + for name in cls.__dict__: + if name.startswith("_rc_"): + rc_methods.add(name) + return rc_methods + + +class Module: + """Represent a module ast.""" + + def __init__(self, name): + self.name = name + self.names = self.get_namespace() + + def get_namespace(self): + fname = self.name + ".py" + filename = os.path.abspath(os.path.join(rendercanvas.__file__, "..", fname)) + with open(filename, "rb") as f: + code = f.read().decode() + + module = ast.parse(code) + + names = {} + for statement in module.body: + if isinstance(statement, ast.ClassDef): + names[statement.name] = statement + elif isinstance(statement, ast.Assign): + if isinstance(statement.targets[0], ast.Name): + name = statement.targets[0].id + if ( + isinstance(statement.value, ast.Name) + and statement.value.id in names + ): + names[name] = names[statement.value.id] + else: + names[name] = statement + return names + + def get_bases(self, class_def): + bases = [] + for base in class_def.bases: + if isinstance(base, ast.Name): + bases.append(base.id) + elif isinstance(base, ast.Attribute): + bases.append(f"{base.value.id}.{base.attr}") + else: + bases.append("unknown") + return bases + + def get_rc_methods(self, class_def): + rc_methods = set() + for statement in class_def.body: + if isinstance(statement, ast.FunctionDef): + if statement.name.startswith("_rc_"): + rc_methods.add(statement.name) + return rc_methods + + def check_rc_methods(self, rc_methods, ref_rc_methods): + too_little = ref_rc_methods - rc_methods + too_many = rc_methods - ref_rc_methods + assert not too_little, too_little + assert not too_many, too_many + print(" all _rc_ methods ok") + + def get_canvas_class(self): + # Check that base names are there + assert "RenderCanvas" in self.names + + # Check canvas + + canvas_class = self.names["RenderCanvas"] + canvas_bases = self.get_bases(canvas_class) + print(f" {canvas_class.name}: {', '.join(canvas_bases)}") + + if "WrapperRenderCanvas" in canvas_bases: + assert "RenderWidget" in self.names + canvas_class = self.names["RenderWidget"] + canvas_bases = self.get_bases(canvas_class) + print(f" {canvas_class.name}: {', '.join(canvas_bases)}") + + return canvas_class + + def check_canvas(self, canvas_class): + rc_methods = self.get_rc_methods(canvas_class) + self.check_rc_methods(rc_methods, canvas_rc_methods) + + def get_loop_class(self): + assert "loop" in self.names + assert "run" in self.names + + loop_statement = self.names["loop"] + assert isinstance(loop_statement, ast.Assign) + loop_class = self.names[loop_statement.value.func.id] + loop_bases = self.get_bases(loop_class) + print(f" loop -> {loop_class.name}: {', '.join(loop_bases)}") + return loop_class + + def check_loop(self, loop_class): + rc_methods = self.get_rc_methods(loop_class) + self.check_rc_methods(rc_methods, loop_rc_methods) + + def get_timer_class(self, loop_class): + timer_class = None + for statement in loop_class.body: + if ( + isinstance(statement, ast.Assign) + and statement.targets[0].id == "_TimerClass" + ): + timer_class = self.names[statement.value.id] + + assert timer_class + timer_bases = self.get_bases(timer_class) + print(f" loop._TimerClass -> {timer_class.name}: {', '.join(timer_bases)}") + + return timer_class + + def check_timer(self, timer_class): + rc_methods = self.get_rc_methods(timer_class) + self.check_rc_methods(rc_methods, timer_rc_methods) + + +canvas_rc_methods = get_ref_rc_methods("RenderCanvas") +timer_rc_methods = get_ref_rc_methods("Timer") +loop_rc_methods = get_ref_rc_methods("Loop") + + +# %% Meta tests + + +def test_meta(): + # Test that all backends are represented in the test. + all_test_names = set(name for name in globals() if name.startswith("test_")) + + dirname = os.path.abspath(os.path.join(rendercanvas.__file__, "..")) + all_modules = set(name for name in os.listdir(dirname) if name.endswith(".py")) + + for fname in all_modules: + if fname.startswith("_"): + continue + module_name = fname.split(".")[0] + test_func_name = f"test_{module_name}_module" + assert ( + test_func_name in all_test_names + ), f"Test missing for {module_name} module" + + +def test_ref_rc_methods(): + # Test basic validity of the reference rc method lists + print(" RenderCanvas") + for x in canvas_rc_methods: + print(f" {x}") + print(" Loop") + for x in loop_rc_methods: + print(f" {x}") + print(" Timer") + for x in timer_rc_methods: + print(f" {x}") + + assert len(canvas_rc_methods) >= 10 + assert len(timer_rc_methods) >= 3 + assert len(loop_rc_methods) >= 3 + + +# %% Test modules that are not really backends + + +def test_base_module(): + # This tests the base classes. This is basically an extra check + # that the method names extracted via the ast match the reference names. + # If this fails on the name matching, something bad has happened. + + m = Module("base") + + canvas_class = m.names["BaseRenderCanvas"] + m.check_canvas(canvas_class) + + m = Module("_loop") + + loop_class = m.names["BaseLoop"] + m.check_loop(loop_class) + + timer_class = m.names["BaseTimer"] + m.check_timer(timer_class) + + +def test_auto_module(): + m = Module("auto") + assert "RenderCanvas" in m.names + assert "loop" in m.names + assert "run" in m.names + + +def test_asyncio_module(): + m = Module("asyncio") + + loop_class = m.names["AsyncioLoop"] + m.check_loop(loop_class) + assert loop_class.name == "AsyncioLoop" + + timer_class = m.get_timer_class(loop_class) + m.check_timer(timer_class) + assert timer_class.name == "AsyncioTimer" + + +# %% Test the backend modules + + +def test_stub_module(): + m = Module("stub") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "StubRenderCanvas" + + loop_class = m.get_loop_class() + m.check_loop(loop_class) + assert loop_class.name == "StubLoop" + + timer_class = m.get_timer_class(loop_class) + m.check_timer(timer_class) + assert timer_class.name == "StubTimer" + + +def test_glfw_module(): + m = Module("glfw") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "GlfwRenderCanvas" + + loop_class = m.get_loop_class() + assert loop_class.name == "GlfwAsyncioLoop" + + # Loop is provided by our asyncio module + assert m.get_bases(loop_class) == ["AsyncioLoop"] + + +def test_qt_module(): + m = Module("qt") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "QRenderWidget" + + loop_class = m.get_loop_class() + m.check_loop(loop_class) + assert loop_class.name == "QtLoop" + + timer_class = m.get_timer_class(loop_class) + m.check_timer(timer_class) + assert timer_class.name == "QtTimer" + + +def test_wx_module(): + m = Module("wx") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "WxRenderWidget" + + loop_class = m.get_loop_class() + m.check_loop(loop_class) + assert loop_class.name == "WxLoop" + + timer_class = m.get_timer_class(loop_class) + m.check_timer(timer_class) + assert timer_class.name == "WxTimer" + + +def test_offscreen_module(): + m = Module("offscreen") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "ManualOffscreenRenderCanvas" + + loop_class = m.get_loop_class() + m.check_loop(loop_class) + assert loop_class.name == "StubLoop" + + timer_class = m.get_timer_class(loop_class) + m.check_timer(timer_class) + assert timer_class.name == "StubTimer" + + +def test_jupyter_module(): + m = Module("jupyter") + + canvas_class = m.get_canvas_class() + m.check_canvas(canvas_class) + assert canvas_class.name == "JupyterRenderCanvas" + + loop_class = m.get_loop_class() + assert loop_class.name == "JupyterAsyncioLoop" + + # Loop is provided by our asyncio module + assert m.get_bases(loop_class) == ["AsyncioLoop"] + + +if __name__ == "__main__": + run_tests(globals()) diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py index 7f5f605..97f17f8 100644 --- a/tests/test_scheduling.py +++ b/tests/test_scheduling.py @@ -10,10 +10,10 @@ class MyTimer(BaseTimer): - def _start(self): + def _rc_start(self): pass - def _stop(self): + def _rc_stop(self): pass @@ -29,10 +29,10 @@ def process_timers(self): if timer.time_left <= 0: timer._tick() - def _run(self): + def _rc_run(self): self.__stopped = False - def _stop(self): + def _rc_stop(self): self.__stopped = True